JavaScriptのDOM操作が遅い原因を詳しく紹介
DOM は非常に遅いので、DOM の操作はできるだけ少なくする必要があるとよく聞いていました。そのため、なぜみんながそう言うのかをさらに詳しく調べたいと思い、ネットでいくつかの情報を学び、ここにまとめました。
まず、DOM オブジェクト自体も js オブジェクトなので、厳密に言えば、このオブジェクトの操作が遅いということではなく、このオブジェクトを操作した後にレイアウトやペイントなどのブラウザーの動作がトリガーされることになります。 。以下では、主にこれらのブラウザーの動作を紹介し、ページが最終的にどのように表示されるかについて説明します。さらに、コードの観点からいくつかの悪い習慣といくつかの最適化ソリューションについても説明します。
ブラウザがページをレンダリングする方法
ブラウザには多くのモジュールがあり、その中で、レンダリング エンジン モジュールは WebKit と Gecko を含みます。ここでは、このモジュールの内容のみを説明します。
まずこのプロセスを言葉で簡単に説明します:
HTMLを解析してDOMツリーを生成します
さまざまなスタイルを解析し、DOMツリーと組み合わせてレンダーツリーを生成します
それぞれのレンダーツリーノードはボックスの位置やサイズなどのレイアウト情報を計算します
レンダーツリーに従って、ブラウザのUIレイヤーを使用して描画されます
DOMツリーとレンダーツリー上のノードは1つではありません「display:none」
などの 1 対 1 の対応は、DOM ツリー上にのみ存在し、このノードを描画する必要がないため、Render ツリーには表示されません。 display:none"
的节点就只会存在于DOM tree上,而不会出现在Render tree上,因为这个节点不需要被绘制。
上图是Webkit的基本流程,在术语上和Gecko可能会有不同,这里贴上Gecko的流程图,不过文章下面的内容都会统一使用Webkit的术语。
影响页面呈现的因素有许多,比如link的位置会影响首屏呈现等。但这里主要集中讨论与layout相关的内容。
paint是一个耗时的过程,然而layout是一个更耗时的过程,我们无法确定layout一定是自上而下或是自下而上进行的,甚至一次layout会牵涉到整个文档布局的重新计算。
但是layout是肯定无法避免的,所以我们主要是要最小化layout的次数。
什么情况下浏览器会进行layout
在考虑如何最小化layout次数之前,要先了解什么时候浏览器会进行layout。
layout(reflow)一般被称为布局,这个操作是用来计算文档中元素的位置和大小,是渲染前重要的一步。在HTML第一次被加载的时候,会有一次layout之外,js脚本的执行和样式的改变同样会导致浏览器执行layout,这也是本文的主要要讨论的内容。
一般情况下,浏览器的layout是lazy的,也就是说:在js脚本执行时,是不会去更新DOM的,任何对DOM的修改都会被暂存在一个队列中,在当前js的执行上下文完成执行后,会根据这个队列中的修改,进行一次layout。
然而有时希望在js代码中立刻获取最新的DOM节点信息,浏览器就不得不提前执行layout,这是导致DOM性能问题的主因。
如下的操作会打破常规,并触发浏览器执行layout:
通过js获取需要计算的DOM属性
添加或删除DOM元素
resize浏览器窗口大小
改变字体
css伪类的激活,比如:hover
通过js修改DOM元素样式且该样式涉及到尺寸的改变
我们来通过一个例子直观的感受下:
// Read var h1 = element1.clientHeight; // Write (invalidates layout) element1.style.height = (h1 * 2) + 'px'; // Read (triggers layout) var h2 = element2.clientHeight; // Write (invalidates layout) element2.style.height = (h2 * 2) + 'px'; // Read (triggers layout) var h3 = element3.clientHeight; // Write (invalidates layout) element3.style.height = (h3 * 2) + 'px';
clientHeight,这个属性是需要计算得到的,于是就会触发浏览器的一次layout。我们来利用chrome(v47.0)的开发者工具看下(截图中的timeline record已经经过筛选,仅显示layout):
上面的例子中,代码首先修改了一个元素的样式,接下来读取另一个元素的clientHeight

上の図はWebkitの基本的な処理であり、Geckoのフローチャートとは用語が異なる場合がありますが、以下の記事内容はWebkitの用語を統一して使用します。

ページのレンダリングに影響を与える要因は数多くあります。たとえば、リンクの位置が最初の画面のレンダリングに影響します。ただし、ここでは主にレイアウト関連のコンテンツに焦点を当てます。 ペイントは時間のかかるプロセスですが、レイアウトはさらに時間がかかります。レイアウトが 1 つであっても、ドキュメント全体の再レイアウトが必要になるため、決定することはできません。 . 計算します。
しかし、レイアウトは絶対に避けられないので、主にレイアウトの数を最小限に抑えたいと考えています。
ブラウザはどのような状況でレイアウトを実行しますか?
レイアウトの数を最小限に抑える方法を検討する前に、まずブラウザがいつレイアウトを実行するかを理解する必要があります。
レイアウト (リフロー) は、一般にレイアウトと呼ばれます。この操作は、ドキュメント内の要素の位置とサイズを計算するために使用されます。これは、レンダリング前の重要なステップです。 HTML が初めて読み込まれるとき、レイアウトに加えて、js スクリプトの実行とスタイルの変更もブラウザーにレイアウトを実行させます。これは、この記事で説明する主な内容でもあります。
🎜 通常の状況では、ブラウザのレイアウトは遅延します。つまり、js スクリプトが実行されるとき、DOM への変更は一時的にキューに保存され、更新されません。コンテキストの実行が完了すると、このキュー内の変更に基づいてレイアウトが実行されます。 🎜🎜ただし、JS コードで最新の DOM ノード情報をすぐに取得したい場合、ブラウザーが事前にレイアウトを実行する必要がある場合があり、これが DOM パフォーマンスの問題の主な原因です。 🎜🎜次の操作はルーチンを中断し、ブラウザでレイアウトを実行するようにトリガーします: 🎜🎜🎜🎜 js を通じて計算する必要がある DOM 属性を取得する 🎜🎜🎜🎜 DOM 要素を追加または削除する 🎜🎜🎜🎜 ブラウザのウィンドウ サイズを変更する🎜🎜🎜🎜フォントを変更する🎜🎜🎜🎜hoverなどのCSS疑似クラスをアクティブにする🎜🎜🎜🎜jsを介してDOM要素のスタイルを変更し、そのスタイルにはサイズ変更が含まれます🎜🎜🎜🎜サンプルを通して直感的に感じてみましょう:🎜🎜🎜// Read var h1 = element1.clientHeight; var h2 = element2.clientHeight; var h3 = element3.clientHeight; // Write (invalidates layout) element1.style.height = (h1 * 2) + 'px'; element2.style.height = (h2 * 2) + 'px'; element3.style.height = (h3 * 2) + 'px';

clientHeight
属性は、以前の変更によりダーティとしてマークされています。この属性を正確に取得できることを確認するために、ブラウザはレイアウトを実行します (Chrome の開発者は、ツールが良心的であるため、このパフォーマンスの問題が発生しました)。 🎜🎜このコードの最適化は非常に簡単で、必要な属性を事前に読み取り、一緒に変更するだけです。 🎜🎜🎜var fragment = document.createDocumentFragment(); for (var i=0; i < items.length; i++){ var item = document.createElement("li"); item.appendChild(document.createTextNode("Option " + i); fragment.appendChild(item); } list.appendChild(fragment);
这个链接里有介绍大部分需要计算的属性:http://www.php.cn/
再来看看别的情况:
面对一系列DOM操作
针对一系列DOM操作(DOM元素的增删改),可以有如下方案:
documentFragment
display: none
cloneNode
比如(仅以documentFragment为例):
var fragment = document.createDocumentFragment(); for (var i=0; i < items.length; i++){ var item = document.createElement("li"); item.appendChild(document.createTextNode("Option " + i); fragment.appendChild(item); } list.appendChild(fragment);
这类优化方案的核心思想都是相同的,就是先对一个不在Render tree上的节点进行一系列操作,再把这个节点添加回Render tree,这样无论多么复杂的DOM操作,最终都只会触发一次layout。
面对样式的修改
针对样式的改变,我们首先需要知道并不是所有样式的修改都会触发layout,因为我们知道layout的工作是计算RenderObject的尺寸和大小信息,那么我如果只是改变一个颜色,是不会触发layout的。
这里有一个网站CSS triggers,详细列出了各个CSS属性对浏览器执行layout和paint的影响。
像下面这种情况,和上面讲优化的部分是一样的,注意下读写即可。
elem.style.height = "100px"; // mark invalidated elem.style.width = "100px"; elem.style.marginRight = "10px"; elem.clientHeight // force layout here
但是要提一下动画,这边讲的是js动画,比如:
function animate (from, to) { if (from === to) return requestAnimationFrame(function () { from += 5 element1.style.height = from + "px" animate(from, to) }) } animate(100, 500)
动画的每一帧都会导致layout,这是无法避免的,但是为了减少动画带来的layout的性能损失,可以将动画元素绝对定位,这样动画元素脱离文本流,layout的计算量会减少很多。
使用requestAnimationFrame
任何可能导致重绘的操作都应该放入requestAnimationFrame
在现实项目中,代码按模块划分,很难像上例那样组织批量读写。那么这时可以把写操作放在requestAnimationFrame
的callback中,统一让写操作在下一次paint之前执行。
// Read var h1 = element1.clientHeight; // Write requestAnimationFrame(function() { element1.style.height = (h1 * 2) + 'px'; }); // Read var h2 = element2.clientHeight; // Write requestAnimationFrame(function() { element2.style.height = (h2 * 2) + 'px'; });
可以很清楚的观察到Animation Frame触发的时机,MDN上说是在paint之前触发,不过我估计是在js脚本交出控制权给浏览器进行DOM的invalidated check之前执行。
其他注意点
除了由于触发了layout而导致性能问题外,这边再列出一些其他细节:
缓存选择器的结果,减少DOM查询。这里要特别提下HTMLCollection。HTMLCollection是通过document.getElementByTagName
得到的对象类型,和数组类型很类似但是每次获取这个对象的一个属性,都相当于进行一次DOM查询:
var ps = document.getElementsByTagName("p"); for (var i = 0; i < ps.length; i++){ //infinite loop document.body.appendChild(document.createElement("p")); }
比如上面的这段代码会导致无限循环,所以处理HTMLCollection对象的时候要做些缓存。
另外,减少DOM元素的嵌套深度并优化css,去除无用的样式对减少layout的计算量有一定帮助。
在DOM查询时,querySelector
和querySelectorAll
应该是最后的选择,它们功能最强大,但执行效率很差,如果可以的话,尽量用其他方法替代。
下面两个jsperf的链接,可以对比下性能。
1)http://www.php.cn/
2)http://www.php.cn/
自己对View层的想法
上面的内容理论方面的东西偏多,从实践的角度来看,上面讨论的内容,正好是View层需要处理的事情。已经有一个库FastDOM来做这个事情,不过它的代码是这样的:
fastdom.read(function() { console.log('read'); }); fastdom.write(function() { console.log('write'); });
问题很明显,会导致callback hell
,并且也可以预见到像FastDOM这样的imperative的代码缺乏扩展性,关键在于用了requestAnimationFrame
后就变成了异步编程的问题了。要让读写状态同步,那必然需要在DOM的基础上写个Wrapper来内部控制异步读写,不过都到了这份上,感觉可以考虑直接上React了……
总之,尽量注意避免上面说到的问题,但如果用库,比如jQuery的话,layout的问题出在库本身的抽象上。像React引入自己的组件模型,用过virtual DOM来减少DOM操作,并可以在每次state改变时仅有一次layout,我不知道内部有没有用requestAnimationFrame
之类的,感觉要做好一个View层就挺有难度的,之后准备学学React的代码。希望自己一两年后会过来再看这个问题的时候,可以有些新的见解。
以上がJavaScriptのDOM操作が遅い原因を詳しく紹介の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

ホットAIツール

Undresser.AI Undress
リアルなヌード写真を作成する AI 搭載アプリ

AI Clothes Remover
写真から衣服を削除するオンライン AI ツール。

Undress AI Tool
脱衣画像を無料で

Clothoff.io
AI衣類リムーバー

Video Face Swap
完全無料の AI 顔交換ツールを使用して、あらゆるビデオの顔を簡単に交換できます。

人気の記事

ホットツール

メモ帳++7.3.1
使いやすく無料のコードエディター

SublimeText3 中国語版
中国語版、とても使いやすい

ゼンドスタジオ 13.0.1
強力な PHP 統合開発環境

ドリームウィーバー CS6
ビジュアル Web 開発ツール

SublimeText3 Mac版
神レベルのコード編集ソフト(SublimeText3)

ホットトピック











WebSocket と JavaScript を使用してオンライン音声認識システムを実装する方法 はじめに: 技術の継続的な発展により、音声認識技術は人工知能の分野の重要な部分になりました。 WebSocket と JavaScript をベースとしたオンライン音声認識システムは、低遅延、リアルタイム、クロスプラットフォームという特徴があり、広く使用されるソリューションとなっています。この記事では、WebSocket と JavaScript を使用してオンライン音声認識システムを実装する方法を紹介します。

WebSocketとJavaScript:リアルタイム監視システムを実現するためのキーテクノロジー はじめに: インターネット技術の急速な発展に伴い、リアルタイム監視システムは様々な分野で広く利用されています。リアルタイム監視を実現するための重要なテクノロジーの 1 つは、WebSocket と JavaScript の組み合わせです。この記事では、リアルタイム監視システムにおける WebSocket と JavaScript のアプリケーションを紹介し、コード例を示し、その実装原理を詳しく説明します。 1.WebSocketテクノロジー

JavaScript と WebSocket を使用してリアルタイム オンライン注文システムを実装する方法の紹介: インターネットの普及とテクノロジーの進歩に伴い、ますます多くのレストランがオンライン注文サービスを提供し始めています。リアルタイムのオンライン注文システムを実装するには、JavaScript と WebSocket テクノロジを使用できます。 WebSocket は、TCP プロトコルをベースとした全二重通信プロトコルで、クライアントとサーバー間のリアルタイム双方向通信を実現します。リアルタイムオンラインオーダーシステムにおいて、ユーザーが料理を選択して注文するとき

WebSocket と JavaScript を使用してオンライン予約システムを実装する方法 今日のデジタル時代では、ますます多くの企業やサービスがオンライン予約機能を提供する必要があります。効率的かつリアルタイムのオンライン予約システムを実装することが重要です。この記事では、WebSocket と JavaScript を使用してオンライン予約システムを実装する方法と、具体的なコード例を紹介します。 1. WebSocket とは何ですか? WebSocket は、単一の TCP 接続における全二重方式です。

JavaScript と WebSocket: 効率的なリアルタイム天気予報システムの構築 はじめに: 今日、天気予報の精度は日常生活と意思決定にとって非常に重要です。テクノロジーの発展に伴い、リアルタイムで気象データを取得することで、より正確で信頼性の高い天気予報を提供できるようになりました。この記事では、JavaScript と WebSocket テクノロジを使用して効率的なリアルタイム天気予報システムを構築する方法を学びます。この記事では、具体的なコード例を通じて実装プロセスを説明します。私たちは

JavaScript チュートリアル: HTTP ステータス コードを取得する方法、特定のコード例が必要です 序文: Web 開発では、サーバーとのデータ対話が頻繁に発生します。サーバーと通信するとき、多くの場合、返された HTTP ステータス コードを取得して操作が成功したかどうかを判断し、さまざまなステータス コードに基づいて対応する処理を実行する必要があります。この記事では、JavaScript を使用して HTTP ステータス コードを取得する方法を説明し、いくつかの実用的なコード例を示します。 XMLHttpRequestの使用

使用法: JavaScript では、insertBefore() メソッドを使用して、DOM ツリーに新しいノードを挿入します。このメソッドには、挿入される新しいノードと参照ノード (つまり、新しいノードが挿入されるノード) の 2 つのパラメータが必要です。

JavaScript は Web 開発で広く使用されているプログラミング言語であり、WebSocket はリアルタイム通信に使用されるネットワーク プロトコルです。 2 つの強力な機能を組み合わせることで、効率的なリアルタイム画像処理システムを構築できます。この記事では、JavaScript と WebSocket を使用してこのシステムを実装する方法と、具体的なコード例を紹介します。まず、リアルタイム画像処理システムの要件と目標を明確にする必要があります。リアルタイムの画像データを収集できるカメラ デバイスがあるとします。
