この記事は 3 日前に github に公開されたものです。質問がある場合は、github に言及してください
ほとんどのユーザーはリソースの読み込み後のパフォーマンスに焦点を当てています。アプリケーションがロードされます。それは特定の用途です。したがって、特にワイヤレス側でユーザーに迅速に対応するには、ブラウザーのレンダリング パフォーマンスを理解する必要があります。
まず第一に、考慮する必要がある質問があります。どのような Web サイトがスムーズですか?次のような一般的な感覚を与えることができるかもしれません: 第 2 レベルの応答など。実際、ユーザーがスムーズだと感じる Web サイトはスムーズである、という非常にお世辞のような答えもできます。ほとんどすべての Web サイトはユーザーをページに留まらせたいと考えているため、ユーザーを中心としたパフォーマンス モデルを構築する必要があります。以下は、Google が提案したユーザー中心のパフォーマンス モデルです。この種のデータは初めてのものではありません (例: ユーザーに応答するには 100 ミリ秒が非常に適切です)。 。
上の図は RAIL の具体的な意味です。 以下にいくつかの主要なデータ インジケーターを示します。
クリティカル レンダリング パス
上の図はブラウザ レンダリングのクリティカル パスです。まず、ブラウザがページを解析することから始めましょう。
アニメーションを作成している場合、通常は JS を使用して対応するスタイルを変更します。その後、ブラウザーは JS の実行、スタイルの計算、レイアウト、描画、合成などの多くの重要な手順を実行します (これについては後で説明します) このステップは実際には長くなる場合も短くなる場合もあります)。次に、実行すべき最適化は、これらのステップで最適化し、中間の時間のかかるステップを削除することです。
JavaScript の実行を最適化する
上の図で説明されている 4 つのシナリオは、ユーザー入力またはアニメーションへの応答に影響を与える可能性があります。関数入力イベント処理、タイミングの悪い JS、長時間にわたる JS 実行、ガベージ コレクション。
まず最初に知っておく必要があるのは、ブラウザは複数の処理プロセス (Compositor、Tile Worker、Main) で構成されているということです。ユーザーがスクロールなどの入力操作 (スクロール、クリックなど) を実行すると、Compositor プロセスはこのイベントを受け取ります (実際には、任意のユーザー入力イベントを受け入れることができます)。可能であれば、メイン プロセスには通知せず、直接処理します。言う:ロール、さあ、赤ちゃん牛。ということで、ページがスクロールしました。もちろん、これには、レイヤーの位置を更新することや、メインスレッドがアイドル状態である間に GPU にフレームを描画させることも含まれます。ただし、そうでないこともよくあります。入力イベントが JS 処理イベントにバインドされている場合、Compositor プロセスはメイン プロセスを積極的にスキップできません。
上の図に示すように、JS によるイベントの処理に時間がかかりすぎる場合、JS の処理が完了するまで入力イベントへの応答はブロックされたままになります。応答が 100 ミリ秒を超えると、ユーザーは遅延を経験します。したがって、ユーザー イベントを処理するときは、次のようにする必要があります:
長時間にわたる JS の実行を避けます。
処理中にスタイルを変更しないでください。スタイルを変更すると、その後のレイアウト、描画、構成、その他の操作が発生するためです。
ユーザー入力をデバウンスします。
その他の最適化:
setTimeout 時間制御によりフレームの途中が発生する可能性があるため、 requestAnimationFrame を使用して、 requestAnimationFrame をより適切にサポートします。
Web ワーカーを使用して複雑な計算された JS を処理するには、Web ワーカーを使用します。
ガベージ コレクションを削減します。ガベージ コレクションの時間が制御されないため、アニメーションの実行がループされる方が理想的です。オブジェクトを再利用します。
DOM 要素の追加または削除、要素属性とスタイル クラスの変更、アニメーション効果の適用などを行うと、DOM 構造が変更されるため、ブラウザは各要素のスタイルを再計算する必要があります。 . 、ページまたはその一部を再レイアウトします (ほとんどの場合)。スタイルを計算する最初のステップは、一致するスタイル セレクターのセットを作成することです。ブラウザはこれを利用して要素にスタイルを適用します。 2 番目のステップでは、一致するスタイル セレクターに基づいて、対応する特定のスタイル ルールを取得し、DOM 要素に適用される最終的な特定のスタイルを計算します。したがって、スタイルの最適化も 2 つのステップです:
セレクターの複雑さを軽減するにはどうすればよいですか?
.box:nth-last-child(-n+1) .title { /* styles */}.final-box-title { /* styles */}
上記のコードはすべて同じ要素を選択します。多数の要素がある場合、2 番目のセレクターのパフォーマンスは最初のセレクターよりも大幅に向上します。 BEM 仕様でも同様のことが行われており、特性に従ってセレクターによって要素を直接選択するパフォーマンスが優れていることがよくあります。
要素の計算量は変更される要素の数に比例するため、無効な要素を減らすことだけに注意する必要があります。
<div> <div> <p>多层无意义的标签</p> </div></div>
上の例のように、冗長なタグが作成される場合があります。外側のスタイルを変更する場合、冗長なタグもスタイル計算する必要があるため、パフォーマンスが無駄になります。
ブラウザが DOM 要素の幾何学的情報 (ページ上のサイズと位置) を計算するプロセス。各要素には、CSS プロパティの設定、要素自体のコンテンツのサイズ、または親要素のサイズによって決定される、明示的または暗黙的なサイズ情報があります。 Blink/WebKit ベースのブラウザおよび IE では、このプロセスはレイアウトと呼ばれます。 Firefox などの Gecko ベースのブラウザでは、このプロセスはリフローと呼ばれます。
現時点では、変換と不透明度は合成のみを引き起こし、レイアウトや再描画は引き起こしません。プロセス全体のうち、パフォーマンスを消費するレイアウトや描画プロセスが直接スキップされるため、パフォーマンスは明らかに非常に優れています。他の CSS プロパティの変更によって引き起こされるプロセスは異なり、一部のプロパティはレイアウトをスキップします。詳細については、「CSS トリガー」を参照してください。したがって、最適化の最初のステップは、レイアウトのトリガーをできる限り回避することです。
Flexbox レイアウト スキームのパフォーマンスは以前のレイアウト スキームよりも向上しており、現在のブラウザーの Flexbox サポートは非常に高くなっています:
最初JS スクリプトを実行し、スタイルの計算、レイアウトの順に実行します。ただし、JS スクリプトを実行する前にブラウザにレイアウト プロセスを強制的に実行させることもできます。これは強制同期レイアウトと呼ばれます。 JS スクリプトの実行時に取得できる要素のスタイル属性値はすべて前のフレームからのものであり、すべて古い値です。したがって、このフレームの先頭にある要素の高さ属性を読み取りたい場合は、次のような JS コードを書くことができます:
function logBoxHeight() { box.classList.add('super-big'); // Gets the height of the box in pixels // and logs it out. console.log(box.offsetHeight);}
为了给你返回 box 的 height 属性值,浏览器必须首先应用 box 的属性修改(因为对其添加了 super-big 样式),接着执行布局过程。在这之后,浏览器才能返回正确的 height 属性值。这样就造成了同步布局事件,是非常消耗性能的。大多数情况下,你应该都不需要先修改然后再读取元素的样式属性值,使用上一帧的值就足够了。过早地同步执行样式计算和布局是潜在的页面性能的瓶颈之一。
function logBoxHeight() { // Gets the height of the box in pixels // and logs it out. console.log(box.offsetHeight); box.classList.add('super-big');}
还有一种情况比强制同步布局更糟:连续快速的多次执行它。
function resizeAllParagraphsToMatchBlockWidth() { // Puts the browser into a read-write-read-write cycle. for (var i = 0; i < paragraphs.length; i++) { paragraphs[i].style.width = box.offsetWidth + 'px'; }}
上述代码对一组段落标签执行循环操作,设置 p 标签的width属性值,使其与 box 元素的宽度相同。看上去这段代码是没问题的,但问题在于,在每次循环中,都读取了 box 元素的一个样式属性值,然后立即使用该值来更新 p 元素的 widt h属性。在下一次循环中读取 box 元素 offsetwidth 属性的时候,浏览器必须先使得上一次循环中的样式更新操作生效,也就是执行布局过程,然后才能响应本次循环中的样式读取操作。布局过程将在每次循环中发生。优化代码:
// Read.var width = box.offsetWidth;function resizeAllParagraphsToMatchBlockWidth() { for (var i = 0; i < paragraphs.length; i++) { // Now write. paragraphs[i].style.width = width + 'px'; }}
如果你想确保编写的读写操作是安全的,你可以使用 FastDOM。它能帮你自动完成读写操作的批处理,还能避免意外地触发强制同步布局或快速连续的布局。
绘制并非总是在内存中的单层画面里完成的。实际上,浏览器在必要时将会把一帧画面绘制成多层画面,然后将这若干层画面合并成一张图片显示到屏幕上。通过渲染层提升可以减小绘制区域,我们可以用调试工具查看到绘制层:
在页面中新建一个渲染层最好的方式就是使用 will-change 属性,同时再与 transform 属性一起使用,就会创建一个新的组合层:
.element { will-change: transform;}
对于那些目前还不支持 will-change 属性、但支持创建渲染层的浏览器,可以使用一个 3D transform 属性来强制浏览器创建一个新的渲染层:
.element { transform: translateZ(0);}
注意: 别盲目创建渲染层,一定要分析其实际性能表现。因为创建渲染层是有代价的,每创建一个新的渲染层,就意味着新的内存分配和更复杂的层的管理。并且在移动端 GPU 和 CPU 的带宽有限制,创建的渲染层过多时,合成也会消耗跟多的时间。
有时候,尽管把元素提升到了一个单独的渲染层,浏览器会把两个相邻区域的渲染任务合并在一起进行,这将导致整个屏幕区域都会被绘制。所以可以使用调试工具查看,仔细规划动画。不同的 CSS 属性绘制的成本是不一样的,绘制一个阴影就比绘制边框更费时。当然,这个浏览器也在不停优化中,现在的耗时渲染属性随时都可能被改变,所以需要多关注一下。
渲染层的合并,就是把页面中完成了绘制过程的部分合并成一层,然后显示在屏幕上。下面和合成相关的两点前面也有提到过。
前面已经提到过 transform/opacity 的优势,应用了 transforms/opacity 属性的元素必须独占一个渲染层。为了对这个元素创建一个自有的渲染层,你必须提升该元素。
创建一个新的渲染层需要消耗额外的内存和管理资源。而在内存资源有限的设备上,由于过多的渲染层来带的开销而对页面渲染性能产生的影响,甚至远远超过了它在性能改善上带来的好处。由于每个渲染层的纹理都需要上传到 GPU 处理,因此我们还需要考虑 CPU 和 GPU 之间的带宽问题、以及有多大内存供 GPU 处理这些纹理的问题。
关注趋势,今天很多的性能瓶颈很可能在将来都不再是问题。如之前关注的一项技术 Web Animations ,是否能用 JS 达到原生动画效果。 Houdini ,你可以添加更多的 JS 代码到动画中而不用担心性能问题。
利用工具 Chrome DevTools
,上面的规则只是优化的方向,善于利用工具分析。移动端利用 inspector 也是非常方便的,并且还可以对数据进行保存,对比分析等。几乎一切需要的分析工具,DevTools 都有。
マイクロ最適化を実行しないでください。短期間で得られるパフォーマンスの向上は非常に小さい場合があります。毎日の高速反復ビジネスでは、これを行う必要はありません。