1723329890
2024-08-06 09:04:53
モダンなバニラ JavaScript シリーズを続けましょう。
アプリを快適にするためにDOMの更新を管理する際に、過剰なメモリ使用を避けるためのベストプラクティスについて説明します。 驚くほど速い™️。
DOM: ドキュメント オブジェクト モデル – 概要
HTML をレンダリングすると、ブラウザでレンダリングされた要素のライブ ビューは DOM と呼ばれます。開発者ツールの「要素」インスペクタに表示されるのは次のようになります。

本質的にはツリーであり、ツリー内の各要素はリーフです。この要素ツリーの変更に特化した API セットが存在します。
一般的な DOM API の簡単なリストを以下に示します。
querySelector()querySelectorAll()createElement()getAttribute()setAttribute()addEventListener()appendChild()
これらは documentのように使います const el = document.querySelector("#el");これらは他のすべての要素でも使用できるため、要素参照がある場合はこれらのメソッドを使用でき、その機能はその要素に限定されます。
const nav = document.querySelector("#site-nav");
const navLinks = nav.querySelectorAll("a");Code language: JavaScript (javascript)
これらのメソッドはブラウザでDOMを変更するために使用できますが、サーバーJavaScriptでは使用できません( Node.js)DOMエミュレータを使用しない限り、 js-dom。
業界として、私たちはこの直接レンダリングのほとんどをフレームワークにオフロードしてきました。すべての JavaScript フレームワーク (React、Angular、Vue、Svelte など) は、これらの API を内部で使用しています。フレームワークの生産性の利点は、手動の DOM 操作による潜在的なパフォーマンスの向上を上回ることが多いことは認識していますが、この記事では、内部で何が行われているのかをわかりやすく説明したいと思います。
そもそもなぜ自分で DOM を操作するのでしょうか?
主な理由はパフォーマンスです。フレームワークは不要なデータ構造を追加して再レンダリングを行う可能性があり、その結果、多くの最新の Web アプリで見られるような、ひどい途切れやフリーズ動作が発生します。これは、ガベージ コレクターがすべてのコードを処理するためにオーバードライブ状態になるためです。
欠点は、DOM 操作を自分で処理するにはコードが増えることです。複雑になる可能性があるため、DOM を手動で操作するよりも、DOM のフレームワークと抽象化を使用する方が開発者にとってより優れたエクスペリエンスになります。いずれにしても、追加のパフォーマンスが必要になる場合があります。それがこのガイドの目的です。
VS Codeは手動のDOM操作に基づいて構築されています
Visual Studio Codeはそのようなケースの1つです。VS Codeは バニラJavaScriptで記述 「可能な限り DOM に近づける」こと。VS Code のような大規模なプロジェクトでは、パフォーマンスを厳密に制御する必要があります。プラグイン エコシステムには多くのパワーがあるため、コアは可能な限りコアかつ軽量である必要があり、それが幅広い採用につながっています。
マイクロソフト Edgeも最近Reactから移行しました 同じ理由です。
直接的な DOM 操作のパフォーマンス (フレームワークを使用するよりも低レベルのプログラミング) が必要な場合は、この記事が役立つでしょう。
より効率的なDOM操作のためのヒント
新しい要素を作成するよりも非表示/表示を優先する
JavaScript を使用して要素を破棄したり作成したりするのではなく、要素を非表示にしたり表示したりして DOM を変更せずに維持する方が、常にパフォーマンスの高いオプションになります。
サーバーは要素をレンダリングし、次のようなクラス(および適切なCSSルールセット)で非表示/表示します。 el.classList.add('show') または el.style.display = 'block' JavaScript を使用して要素を動的に作成して挿入する代わりに、ほとんど静的な DOM では、ガベージ コレクションの呼び出しや複雑なクライアント ロジックがないため、パフォーマンスが大幅に向上します。
回避できる場合は、クライアント上で DOM ノードを動的に作成しないでください。
しかし、支援技術を忘れないでください。視覚的にも支援技術にも隠したい要素がある場合は、 display: none; そうすればいいのですが、要素を非表示にして補助技術のためにそのままにしておきたい場合は、 コンテンツを非表示にする他の方法。
好む textContent 以上 innerText 要素の内容を読み取る
の innerText このメソッドは、要素の現在のスタイルを認識しているため優れています。要素が非表示かどうかを認識し、実際に何かが表示されている場合にのみテキストを取得します。問題は、スタイルをチェックするこのプロセスによってリフローが強制され、速度が遅くなることです。
コンテンツを読む element.textContent よりはるかに速い element.innerText、好む textContent 可能な場合は要素の内容を読み取ります。
使用 insertAdjacentHTML 以上 innerHTML
の insertAdjacentHTML この方法は、 innerHTML 挿入する前に DOM を破棄する必要がないためです。 方法 新しい HTML を配置する場所は柔軟です。たとえば、次のようになります。
el.insertAdjacentHTML("afterbegin", html);
el.insertAdjacentHTML("beforeend", html);Code language: JavaScript (javascript)
最も早いアプローチは insertAdjacentElement または appendChild
アプローチ1: template HTMLテンプレートを作成するタグと appendChild 新しいHTMLを挿入する
最も速い方法は、完全なDOM要素を追加することです。このための確立されたパターンは、 タグを使用して要素を作成し、それをDOMに挿入します。 insertAdjacentElement または appendChild 方法。
template id="card_template">
article class="card">
h3>h3>
div class="card__body">
div class='card__body__image'>div>
section class='card__body__content'>
section>
div>
article>
template>Code language: HTML, XML (xml)
function createCardElement(title, body) {
const template = document.getElementById('card_template');
const element = template.content.cloneNode(true).firstElementChild;
const [cardTitle] = element.getElementsByTagName("h3");
const [cardBody] = element.getElementsByTagName("section");
[cardTitle.textContent, cardBody.textContent] = [title, body];
return element;
}
container.appendChild(createCardElement(
"Frontend System Design: Fundamentals",
"This is a random content"
))Code language: JavaScript (javascript)
この動作は新しい フロントエンドシステム設計コース、Evgenni が無限スクロールのソーシャル ニュース フィードをゼロから構築します。
アプローチ2: 使用 createDocumentFragment と appendChild バッチ挿入
DocumentFragment DOMノードを保持できる軽量の「空の」ドキュメントオブジェクトです。アクティブなDOMツリーの一部ではないため、 複数 挿入する要素。
const fragment = document.createDocumentFragment();
for (let i = 0; i 1000; i++) {
const li = document.createElement('li');
li.textContent = `Item ${i}`;
fragment.appendChild(li);
}
document.getElementById('myList').appendChild(fragment);Code language: JavaScript (javascript)
このアプローチでは、すべての要素を個別に挿入するのではなく、一度に挿入することで、リフローと再描画を最小限に抑えます。
ノードが削除されたときに参照を管理する
DOMノードを削除するときに、ガベージコレクターが関連データをクリーンアップするのを妨げる参照が残ってほしくありません。 WeakMap そして WeakRef 参照漏れを防ぐためです。
DOMノードにデータを関連付ける WeakMap
DOMノードにデータを関連付けるには、 WeakMapこうすることで、後で DOM ノードが削除されても、データへの参照は完全に失われます。
let DOMdata = { 'logo': 'Frontend Masters' };
let DOMmap = new WeakMap();
let el = document.querySelector(".FmLogo");
DOMmap.set(el, DOMdata);
console.log(DOMmap.get(el));
el.remove(); Code language: JavaScript (javascript)
弱いマップを使用すると、DOM 要素が削除されてもデータへの参照が残らないようになります。
ガベージコレクション後のクリーンアップ WeakRef
次の例では、 WeakRef DOMノードに:
class Counter {
constructor(element) {
this.ref = new WeakRef(element);
this.start();
}
start() {
if (this.timer) {
return;
}
this.count = 0;
const tick = () => {
const element = this.ref.deref();
if (element) {
console.log("Element is still in memory, updating count.")
element.textContent = `Counter: ${++this.count}`;
} else {
console.log("Garabage Collector ran and element is GONE – clean up interval");
this.stop();
this.ref = null;
}
};
tick();
this.timer = setInterval(tick, 1000);
}
stop() {
if (this.timer) {
clearInterval(this.timer);
this.timer = 0;
}
}
}
const counter = new Counter(document.getElementById("counter"));
setTimeout(() => {
document.getElementById("counter").remove();
}, 5000);Code language: JavaScript (javascript)
ノードを削除した後、コンソールを監視して実際のガベージ コレクションがいつ発生するかを確認するか、開発者ツールの[パフォーマンス]タブを使用してガベージ コレクションを強制的に実行することもできます。

そうすれば、すべての参照が削除され、タイマーがクリーンアップされたことを確認できます。
注意: WeakRef を過度に使用しないようにしてください。この魔法にはコストがかかります。参照を明示的に管理できると、パフォーマンスが向上します。
イベントリスナーのクリーンアップ
手動でイベントを削除する removeEventListener
function handleClick() {
console.log("Button was clicked!");
el.removeEventListener("click", handleClick);
}
const el = document.querySelector("#button");
el.addEventListener("click", handleClick);Code language: JavaScript (javascript)
使用 once 一度だけ行うイベントのパラメータ
上記と同じ動作は、「once」パラメータを使用して実現できます。
el.addEventListener('click', handleClick, {
once: true
});Code language: JavaScript (javascript)
3番目のパラメータを追加する addEventListener リスナーが最大で呼び出されることを示すブール値を持つ 一度 追加された後、リスナーは呼び出されると自動的に削除されます。
イベント委任を使用して、バインドするイベントの数を減らす
非常に動的なコンポーネントでノードを頻繁に構築および置換する場合、ノードを構築するときにそれぞれのイベント リスナーをすべてセットアップする必要があるため、コストが高くなります。
代わりに、ルートレベルに近いイベントをバインドすることができます。イベントはDOMをバブルアップするので、 event.target (イベントの元のターゲット) イベントをキャッチして応答します。
使用 matches(selector) 現在の要素にのみ一致するため、リーフ ノードである必要があります。
const rootEl = document.querySelector("#root");
rootEl.addEventListener('click', function (event) {
if (event.target.matches('.target-element')) doSomething();
});Code language: JavaScript (javascript)
おそらく次のような要素があるでしょう in this case you’d need to use the .closest(element) 方法。
const rootEl = document.querySelector("#root");
rootEl.addEventListener('click', function (event) {
if (event.target.closest('.target-element')) doSomething();
});Code language: JavaScript (javascript)
この方法を使用すると、要素を動的に挿入した後でリスナーの追加や削除を心配する必要がなくなります。
AbortControllerを使用してイベントのグループをアンバインドする
const button = document.getElementById('button');
const controller = new AbortController();
const { signal } = controller;
button.addEventListener(
'click',
() => console.log('clicked!'),
{ signal }
);
controller.abort();Code language: JavaScript (javascript)
あなたは AbortController イベントのセットを削除します。
let controller = new AbortController();
const { signal } = controller;
button.addEventListener('click', () => console.log('clicked!'), { signal });
window.addEventListener('resize', () => console.log('resized!'), { signal });
document.addEventListener('keyup', () => console.log('pressed!'), { signal });
controller.abort();Code language: JavaScript (javascript)
アレックス・マッカーサーに感謝 AbortController コード例 これについては。
プロファイリングとデバッグ
DOMを測定して確認しましょう 大きすぎない。
メモリ プロファイリングに Chrome DevTools を使用する方法についての簡単なガイドを以下に示します。
- Chrome DevToolsを開く
- 「メモリ」タブに移動します
- 「ヒープスナップショット」を選択し、「スナップショットを取得」をクリックします。
- DOM操作を実行する
- もう一度スナップショットを撮る
- スナップショットを比較してメモリの増加を特定する

注目すべき重要な点:
- 予期せず保持されたDOM要素
- クリーンアップされていない大きな配列またはオブジェクト
- 時間の経過とともにメモリ使用量が増加する(潜在的なメモリリーク)
「パフォーマンス」タブを使用して、時間の経過に伴うメモリ使用量を記録することもできます。
- 「パフォーマンス」タブに移動します
- オプションで「メモリ」をチェックする
- 「記録」をクリック
- DOM操作を実行する
- 記録を停止し、メモリグラフを分析する
これにより、メモリ割り当てを視覚化し、DOM 操作中の潜在的なリークや不要な割り当てを特定できるようになります。
JavaScript 実行時間分析
メモリ プロファイリングに加えて、Chrome DevTools のパフォーマンス タブは、DOM 操作コードを最適化するときに重要な JavaScript 実行時間を分析するのに非常に役立ちます。
使い方は次のとおりです:
- Chrome DevToolsを開き、「パフォーマンス」タブに移動します
- 録画ボタンをクリック
- 分析したいDOM操作を実行する
- 録音を停止

結果のタイムラインには次の内容が表示されます。
- JavaScript 実行 (黄色)
- レンダリングアクティビティ(紫)
- 絵画(緑)
探す:
- 長い黄色のバーは、時間のかかる JavaScript 操作を示します。
- 頻繁に短い黄色のバーが表示される場合、DOM 操作が過剰である可能性があります。
さらに詳しく:
- 黄色のバーをクリックすると、特定の関数呼び出しとその実行時間が表示されます。
- 「ボトムアップ」タブと「コールツリー」タブを見て、どの関数に最も時間がかかっているかを特定します。
この分析により、DOM 操作コードがパフォーマンスの問題を引き起こしている場所を正確に特定し、ターゲットを絞った最適化が可能になります。
パフォーマンスデバッグリソース
Chrome 開発チームによる記事:
メモリとパフォーマンスの分析、Chrome Dev Tools についてさらに詳しく説明するコース:
効率的な DOM 操作は、適切な方法を使用するだけでなく、DOM といつ、どのくらいの頻度でやり取りするかを理解することも重要です。効率的な方法を使用しても、操作を過度に行うとパフォーマンスの問題が発生する可能性があります。
DOM最適化の重要なポイント
パフォーマンスが重視される Web アプリを作成する場合、効率的な DOM 操作の知識が重要です。最新のフレームワークは利便性と抽象化を提供しますが、これらの低レベルのテクニックを理解して適用すると、特に要求の厳しいシナリオでアプリのパフォーマンスを大幅に向上させることができます。
要約すると次のようになります:
- 可能な場合は、新しい要素を作成するよりも、既存の要素を変更することを優先します。
- 次のような効率的な方法を使用する
textContent、insertAdjacentHTML、 そしてappendChild。 - 参照を慎重に管理し、
WeakMapそしてWeakRefメモリリークを回避するためです。 - 不要なオーバーヘッドを防ぐために、イベント リスナーを適切にクリーンアップします。
- より効率的なイベント処理のために、イベント委任などの手法を検討してください。
- 次のようなツールを使用する
AbortController複数のイベント リスナーの管理が容易になります。 - 雇用する
DocumentFragmentバッチ挿入の s を理解し、より広範な最適化戦略のための仮想 DOM などの概念を理解します。
覚えておいてください。目標は、必ずしもフレームワークを放棄して、すべてのプロジェクトで DOM を手動で操作することではありません。むしろ、これらの原則を理解して、フレームワークを使用するタイミングと、より低いレベルで最適化するタイミングについて、情報に基づいた決定を下せるようにすることです。メモリ プロファイリングやパフォーマンス ベンチマークなどのツールは、これらの決定に役立ちます。
#最新のバニラ #JavaScript #によるメモリ効率の高い #DOM #操作のパターン #Frontend #Masters #Boost