フロントエンド開発はバックエンドとは異なり、多数のアルゴリズムを使用するシナリオはほとんどありませんが、フロントエンドのパフォーマンスも最適化する必要があります。適切なコードは、Web ページの安定した高パフォーマンスの動作を確保するための基礎です。この記事では、これまでの開発で発生したシナリオと組み合わせて、フロントエンド Web ページがフリーズする原因を整理および分析し、対応する解決策を示します。
フロントエンド ページがフリーズする理由は数多くありますが、レンダリング メカニズムと操作から次の 2 つのカテゴリに分類できます。
-
レンダリングがタイムリーでなく、ページがフレームをドロップします
-
Webページのメモリ使用量が多すぎて動作がフリーズする
2 つのタイプは次のように細分化できます。
レンダリングがタイムリーでなく、ページがフレームをドロップします
js スレッドの長時間占有、ページのリフローと再描画、多くのリソース読み込みブロック
過剰なメモリが原因でページがフリーズする
メモリリークにより過剰なメモリが発生する
-
予期しないグローバル変数が原因でメモリ リークが発生する
-
クロージャによるメモリリーク
-
忘れられたタイマー
-
循環参照
-
DOM が削除されたときにバインド解除イベントはありません
-
サニタイズされていない DOM 要素への参照
DOM ノードまたはイベントが大量のメモリを占有している
1. レンダリング
1. jsスレッドを長時間占有する
ブラウザには js スレッドと GUI スレッドがあり、この 2 つは相互に排他的であるため、js スレッドが長時間占有されるとレンダリングが間に合わず、ページがフリーズします。
例 1:
document.body.html('为什么不先渲染我');
//程序
$.ajax({
url: '',
async: false
})
//运行结果会在ajax执行完毕后,再去渲染页面
同期方法でデータを取得すると、GUI スレッドがハングし、データが返された後にレンダリングが実行されます。
例 2:
function a (){
// arr的长度过长时会导致页面卡顿
arr.forEach(item => {
...
})
}
let inputDom = document.getElementById('input')
let arr = document.getElementsByClassName('.tag')
input.onchange = a
計算時間が長すぎるため、ページのレンダリングが間に合わない
レンダリングが遅れる理由:
ブラウザのレンダリング周波数は一般的に 60HZ、つまり 1 フレームに必要な時間は 1s / 60 = 16.67ms です。ブラウザがページを表示するとき、js ロジックを処理してレンダリングする必要があり、各実行セグメントは 16.67 を超えることはできません。 MS。実際、ブラウザカーネル独自のサポートシステムの動作にも時間がかかるため、私たちに残された時間はわずか 10ms 程度です。
一般的な最適化方法:
-
requestIdleCallback と requestAnimationFrame を使用したタスクの断片化
例
function Task(){
this.tasks = [];
}
//添加一个任务
Task.prototype.addTask = function(task){
this.tasks.push(task);
};
//每次重绘前取一个task执行
Task.prototype.draw = function(){
var that = this;
window.requestAnimationFrame(function(){
var tasks = that.tasks; if(tasks.length){
var task = tasks.shift();
task();
}
window.requestAnimationFrame(function(){that.draw.call(that)});
});
};
2. ページのリフローと再描画が多数あります
-
レイアウトを最小化する
cancelTop や clentWidth などのディメンション属性を取得する場合、レイアウトがトリガーされてリアルタイム値を取得するため、これらの値は for ループでキャッシュされる必要があります
例:
最適化前
for(var i = 0; i < childs.length; i++){
childs.style.width = node.offsetWidth + "px";
}
最適化後
var width = node.offsetWidth;for(var i = 0; i < childs.length; i++){
childs.style.width = width + "px";
}
-
DOM 構造を簡素化する
DOM 構造が複雑になると、より多くの要素を再描画する必要があります。したがって、特にアニメーション化するもの、またはスクロール/マウス移動イベントをリッスンするものでは、dom はシンプルにしておく必要があります。さらに、flex を使用すると、float を使用するよりも再描画に利点があります。
3. リソース読み込みのブロック
-
jsリソースは本文の前に配置されます
-
インラインスクリプトのブロック
-
CSS の読み込みは DOM ツリーのレンダリングをブロックします (CSS は DOM ツリーの解析をブロックしません)
-
リソースブロックが多すぎる
2. メモリ過剰によるページのフリーズ
1. メモリリークにより過剰なメモリが発生する
ブラウザには独自のガベージ コレクション メカニズムがあり、主流のガベージ コレクション メカニズムはマーク クリアです。ただし、IE でネイティブ DOM にアクセスすると、参照カウント メカニズムが使用されます。アイドル状態のメモリが時間内に回復されないと、メモリ リークが発生します。
2 つのガベージ コレクション メカニズム (GC ガベージ コレクション) を簡単に紹介します。
マークをクリアします:
定義と使用法:
変数が環境に入るときは、変数に「環境に入る」とマークし、変数が環境から出るときは、「環境から出る」とマークします。ある時点で、ガベージ コレクターは環境内の変数と環境変数によって参照される変数をフィルターで除外し、残りはリサイクルの準備ができていると見なされる変数になります。
これまでのところ、IE、Firefox、Opera、Chrome、Safari の JS 実装はすべて、マークアンドスイープ ガベージ コレクション戦略または同様の戦略を使用していますが、ガベージ コレクションの間隔はそれぞれ異なります。
プロセス:
-
ブラウザの実行中、メモリに保存されているすべての変数にマークが付けられます。
-
環境内の変数と環境内で参照されている変数を削除します。
-
マークされた変数がまだある場合、それは削除される変数とみなされます。
-
ガベージ コレクション メカニズムはメモリのクリア作業を完了し、マークされた変数を破棄し、変数が占有しているメモリ領域を再利用します。
参照カウント
定義と使用法: 参照カウントは、各値が参照された回数を追跡することです。
基本原理:変数の参照回数であり、1回参照されると1加算され、参照回数が0の場合は再利用可能とみなされる
物体。
プロセス
-
変数が宣言され、その変数に参照型の値が代入されています。この参照型の値への参照数は 1 です。
-
同じ値が別の変数に代入され、この参照型の値参照の数が 1 増加します。
-
この参照型の値を含む変数に別の値が割り当てられると、この参照型の値の参照カウントが 1 減ります。
-
参照数が 0 になると、値を逆参照する必要があることを意味します
-
次回ガベージ コレクション メカニズムが実行されると、参照カウントが 0 の値によって占有されていたメモリが解放されます。
メモリリークの一般的な原因:
-
予期しないグローバル変数が原因でメモリ リークが発生する
解決策: 厳密モードの使用を避けてください。
例
<button onclick="createNode()">添加节点</button>
<button onclick="removeNode()">删除节点</button>
<div id="wrapper"></div>
<script>
var text = [];
function createNode() {
text.push(new Array(1000000).join('x'));
var textNode = document.createTextNode("新节点"),
div = document.createElement('div');
div.appendChild(textNode);
document.getElementById("wrapper").appendChild(div);
}
function removeNode() {
var wrapper = document.getElementById("wrapper"),
len = wrapper.childNodes.length;
if (len > 0) {
wrapper.removeChild(wrapper.childNodes[len - 1]);
}
}
</script>
テキスト変数は createNode で参照されるため、テキストは再利用できません
-
クロージャによるメモリリーク
例:
<button onclick="replaceThing()">第二次点我就有泄漏</button>
<script>
var theThing = null;
var replaceThing = function () {
var originalThing = theThing;
var unused = function () {
if (originalThing) {
console.log("hi");
};
}
theThing = {
longStr: new Array(1000000).join('*'),
someMethod: function someMethod() {
console.log('someMessage');
}
};
};
上記のコードがリークする理由は、unused と someMethod という 2 つのクロージャがあり、どちらも親スコープを共有しているためです。
theThing はグローバル変数であり、someMethod はグローバル変数のプロパティであるため、それが参照するクロージャ スコープ (unused と somMethod で共有) は解放されません。originalThing は共有スコープ内にあるため、originalThing は解放されません。呼び出しでは、originalThing は前の theThing を指し、新しい theThing.someMethod はoriginalThing を参照するため、クロージャ参照チェーンが形成されます。longStr は解放できない大きな文字列であるため、メモリ リークが発生します。
解決策: replaceThing の最後にoriginalThing = null を追加します。
-
忘れられたタイマー
例
var someResource = getData();
setInterval(function() {
var node = document.getElementById('Node');
if(node) {
// 处理 node 和 someResource
node.innerHTML = JSON.stringify(someResource));
}
}, 1000);
タイマー コールバック関数はリサイクルされません (タイマーは停止時にリサイクルされます)。
-
循環参照
循環参照とは、オブジェクト A にオブジェクト B への別のポインタが含まれており、そのポインタにも A への参照が含まれている場合です。
IE での BOM と DOM の実装では COM が使用され、COM オブジェクトによって使用されるガベージ コレクション メカニズムは参照カウント方式であるためです。したがって、循環参照の問題が発生します
解決策: js オブジェクトと DOM の間のリンクを手動で切断します。割り当てられた値は null です。
例:
function handle () {
var element = document.getElementById(“testId”);
element.onclick = function (){
alert(element.id)
}
}
要素のプロパティは、要素によってバインドされたイベント内で参照されます。
onclick イベントはクロージャであり、関数内のローカル変数を解放できないように維持できます。つまり、要素変数を解放できず、要素が呼び出されるたびに解放されず、最終的にメモリ リークが発生します。
解決:
function handle () {
var element = document.getElementById(“testId”);
element.onclick = function (){
alert(element.id)
}
element = null
}
-
DOM が削除されたときにバインド解除イベントはありません
たとえば、ボタンを削除しても、ボタン上のイベントは解放されません。
-
サニタイズされていない DOM 要素への参照
2. Dom ノードまたはイベントが大量のメモリを占有している
詳細な分析については、私の他の記事「Web ページ上の
例:
function addDom(){
let d = document.createDocumentFragment();
for(var i = 0;i<30;i++){
let li = document.createElement('li')
li.addEventListener('click', function(e) {
let _this = e.target;
let dom = e.target.tagName.toLowerCase();
_this.style.color = 'red';
})
li.innerHTML = `</div>
<h4>
测试图片
</h4>
<img style = "height:20px;width:100px" src="https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1591105123139&di=90a63b4962d0b4405ce65c2096a676c2&imgtype=0&src=http%3A%2F%2Fimg0.imgtn.bdimg.com%2Fit%2Fu%3D3769023270%2C3433174748%26fm%3D214%26gp%3D0.jpg"/>
</div>`
d.appendChild(li)
}
document.getElementById('app').appendChild(d)
}
上記のコードはプルダウン読み込みであり、毎回 dom が追加されるため、最終的には過剰なメモリが発生します。
解決策: 仮想リストとイベント委任を使用する
概要:実際の開発プロセスでは、ページ フリーズが発生するシナリオが多数あります。メモリ リーク検出ツール (IE の場合は sIEve) を使用して検出することも、Chrome が提供するタイムライン、プロファイル、またはパフォーマンスを使用することもできます。ここで詳しく説明します。