ホームページ > 記事 > ウェブフロントエンド > ブラウザでページをレンダリングする方法をまとめます
Webの基礎から転載
ブラウザがページをレンダリングする前に、まずDOMとCSSOMツリーを構築する必要があります。したがって、HTML と CSS の両方ができるだけ早くブラウザーに提供されるようにする必要があります。
バイト→キャラクター→タグ→ノード→オブジェクトモデル。
HTML マークアップはドキュメント オブジェクト モデル (DOM) に変換され、CSS マークアップは CSS オブジェクト モデル (CSSOM) に変換されます。 DOM と CSSOM は独立したデータ構造です。
Chrome DevTools タイムラインは、DOM と CSSOM の構築と処理のオーバーヘッドをキャプチャして検査できます。
<html> <head><meta name="viewport" content="width=device-width,initial-scale=1"><link href="style.css?1.1.11" rel="stylesheet"><title>Critical Path</title> </head> <body><p>Hello <span>web performance</span> students!</p><div><img src="awesome-photo.jpg"></div> </body></html>
テキストと画像を含む通常の HTML ページ、ブラウザはこのページをどのように処理しますか?
HTML パーサーによって出力されるツリーは、DOM 要素と属性ノードで構成され、HTML ドキュメントのオブジェクト記述であり、HTML 要素と外部世界 (JavaScript など) の間のインターフェイスでもあります。 DOM とタグはほぼ 1 対 1 で対応します。
変換: ブラウザは、ディスクまたはネットワークから HTML の生のバイトを読み取り、ファイルの指定されたエンコーディング (UTF-8 など) に従って個々の文字に変換します。
トークン化: ブラウザは、文字列を、「」、「
」、山かっこ内のその他の文字列など、W3C HTML5 標準で指定されているさまざまなトークンに変換します。各トークンには特別な意味と一連のルールがあります。字句解析: 発行されたトークンは、そのプロパティとルールを定義する「オブジェクト」に変換されます。
DOM Build: 最後に、HTML タグは異なるタグ間の関係を定義するため (一部のタグは他のタグ内に含まれます)、作成されたオブジェクトは、元の親子関係もキャプチャするツリー データ構造内でリンクされます。マークアップで定義: HTML オブジェクトは body オブジェクトの親、body は paragraph オブジェクトの親など。
プロセス全体の最終出力は、ページのドキュメント オブジェクト モデル (DOM) であり、これはページの以降のすべての処理でブラウザによって使用されます。
ブラウザは HTML マークアップを処理するたびに、バイトを文字に変換し、トークンを決定し、トークンをノードに変換して、DOM ツリーを構築する上記の手順をすべて完了します。特に処理する HTML が大量にある場合、このプロセス全体が完了するまでに時間がかかることがあります。
Chrome DevTools を開いてページの読み込み時のタイムラインを記録すると、このステップの実行に実際にどれくらい時間がかかるかを確認できます。上の例では、大量の HTML バイトを DOM ツリーに変換するのに約 5 ミリ秒かかります。ページが大きい場合、このプロセスに必要な時間が大幅に長くなる可能性があります。スムーズなアニメーションを作成する場合、ブラウザが大量の HTML を処理する必要がある場合、これがボトルネックになりやすいです。
DOM ツリーはドキュメント タグのプロパティと関係をキャプチャしますが、レンダリング時に要素がどのように見えるかは教えてくれません。それは CSSOM の責任です。
この単純なページの DOM を構築する過程で、ブラウザーは、外部 CSS スタイル シート style.css を参照するドキュメントの先頭にリンク タグを検出しました。ページをレンダリングするためにリソースが必要になることを予期して、即座にリソースに対してリクエストを行い、以下を返します:
body { font-size: 16px }p { font-weight: bold }span { color: red }p span { display: none }img { float: right }
HTML マークアップ内 (インライン) でスタイルを直接宣言することもできましたが、CSS を HTML から独立させることで、コンテンツとデザインを独立した関心事として扱うことができます。デザイナーは CSS を扱い、開発者は HTML に集中するなどです。
HTML を扱うときと同じように、受け取った CSS ルールをブラウザーが理解して処理できるものに変換する必要があります。したがって、HTML プロセスを繰り返しますが、HTML ではなく CSS の場合:
CSS バイトは文字に変換され、次にトークンとノードに変換され、最後に「CSS オブジェクト モデル」(CSSOM) と呼ばれるものにリンクされます。ツリー構造:
CSSOM はなぜツリー構造なのでしょうか?ページ上のノード オブジェクトのスタイルの最終セットを計算するとき、ブラウザは、ノードに適用される最も一般的なルールから開始し (たとえば、ノードが body 要素の子の場合、すべての body スタイルを適用します)、次にパス より具体的なルールを適用して、計算のスタイルを再帰的に最適化します。
より具体的に説明するために、上記の CSSOM ツリーを例として取り上げます。 body 要素内の span タグ内に配置されたテキストは、16 ピクセルのフォント サイズと赤色になります。フォント サイズ ディレクティブは、本文からスパンまでカスケードされます。ただし、span タグが段落 (p) タグの子である場合、その内容は表示されません。
また、上記のツリーは完全な CSSOM ツリーではなく、スタイルシートでオーバーライドすることを決定したスタイルのみを示していることに注意してください。すべてのブラウザーにはデフォルト スタイル (「ユーザー エージェント スタイル」とも呼ばれます) のセットが提供されており、これが当社のスタイルです。これらのデフォルトのスタイルをオーバーライドするだけです。
CSS 処理にかかる時間を理解するには、DevTools でタイムラインを記録し、「スタイルの再計算」イベントを探します。DOM 解析とは異なり、タイムラインには個別の「CSS 解析」エントリは表示されず、代わりに解析と CSSOM ツリーがキャプチャされます。
この小さなスタイルシートの処理には約 0.6 ミリ秒かかり、ページ上の 8 つの要素に影響を及ぼします。それほど多くはありませんが、それでもオーバーヘッドが発生します。しかし、これらの 8 つの要素はどこから来たのでしょうか? DOM と CSSOM を結び付けるのはレンダー ツリーです。
CSSOM ツリーと DOM ツリーはレンダリング ツリーにマージされ、これを使用して各表示要素のレイアウトが計算され、描画プロセスに出力されてピクセルが画面にレンダリングされます。 。最適なレンダリング パフォーマンスを達成するには、これらの各ステップを最適化することが重要です。
ブラウザは、HTML と CSS の入力に基づいて DOM ツリーと CSSOM ツリーを構築します。 ただし、これらは完全に独立したオブジェクトであり、ドキュメントのさまざまな側面をキャプチャします。1 つはコンテンツを記述し、もう 1 つはドキュメントに適用する必要があるスタイル ルールを記述します。この 2 つをマージしてブラウザに画面上のピクセルをレンダリングさせるにはどうすればよいでしょうか?
DOM ツリーは CSSOM ツリーとマージされて、Web ページのレンダリングに必要なノードのみを含むレンダリング ツリーを形成します。各 DOM ツリー内のノードを走査し、CSSOM ルール ツリー内の現在のノードのスタイルを見つけて、レンダリング ツリーを生成します。
Layout は、各オブジェクトの正確な位置とサイズを計算します。
最後のステップは描画であり、最終レンダー ツリーを使用してピクセルを画面にレンダリングします。
最初のステップは、ブラウザーに DOM と CSSOM を「レンダリング ツリー」にマージさせ、Web ページ上のすべての 表示 DOM コンテンツ とすべての CSSOM スタイル情報をカバーするようにすることです。各ノードの 。
レンダリング ツリーを構築するために、ブラウザは通常、次の作業を完了します:
DOM ツリーのルート ノードから開始して、表示されている各ノードをトラバースします。
一部のノード (スクリプト タグ、メタ タグなど) は表示されず、レンダリングされた出力に反映されないため無視されます。
一部のノードは CSS によって非表示になっているため、レンダー ツリーでは無視されます。たとえば、「display: none」属性はスパン ノードに設定されているため、レンダリング ツリーには表示されません。
遍历每个可见节点,为其找到适配的 CSSOM 规则并应用它们。从选择器的右边往左边开始匹配,也就是从CSSOM树的子节点开始往父节点匹配。
Emit visible nodes with content and their computed styles.
注: visibility: hidden 与 display: none 是不一样的。前者隐藏元素,但元素仍占据着布局空间(即将其渲染成一个空框),而后者 (display: none) 将元素从渲染树中完全移除,元素既不可见,也不是布局的组成部分。
最终输出的渲染同时包含了屏幕上的所有可见内容及其样式信息。有了渲染树,我们就可以进入“布局”阶段。
到目前为止,我们计算了哪些节点应该是可见的以及它们的计算样式,但我们尚未计算它们在设备视口内的确切位置和大小---这就是“布局”阶段,也称为“reflow”。
为弄清每个对象在网页上的确切大小和位置,浏览器从渲染树的根节点开始进行遍历。让我们考虑一个简单的实例:
<html> <head><meta name="viewport" content="width=device-width,initial-scale=1"><title>Critial Path: Hello world!</title> </head> <body><div style="width: 50%"> <div style="width: 50%">Hello world!</div></div> </body></html>
上記の Web ページの本文には 2 つのネストされた div が含まれています。最初の (親) div はノードの表示サイズをビューポートの幅の 50% に設定し、親 div には幅の 50% の 2 番目の div が含まれています。親、これはビューポート幅の 25% です。
レイアウト プロセスの出力は、ビューポート内の各要素の正確な位置とサイズを正確にキャプチャする「ボックス モデル」です。すべての相対測定値は画面上の絶対ピクセルに変換されます。
最後に、どのノードが表示されているか、その計算されたスタイル、およびジオメトリ情報がわかったので、この情報を最終段階、つまりレンダー ツリー内の各ノードを画面上の実際のピクセルに変換する段階に渡すことができます。このステップは、多くの場合、「ペイント」または「ラスタライズ」と呼ばれます。
Chrome DevTools は、上記の 3 つの段階すべてで消費される時間を深く理解するのに役立ちます。元の「hello world」例のレイアウト フェーズを見てみましょう:
「Layout」イベントは、タイムラインでのレンダー ツリーの構築、位置、およびサイズの計算をキャプチャします。
レイアウトが完了すると、ブラウザは、レンダー ツリーを画面上のピクセルに変換する「ペイント セットアップ」イベントと「ペイント」イベントを発行します。
レンダー ツリーの構築、レイアウト、ペイントの実行に必要な時間は、ドキュメントのサイズ、適用されるスタイル、および設定によって異なります。ドキュメントが実行されているデバイス: ドキュメントが大きくなるほど、ブラウザが実行しなければならない作業が増え、スタイルが複雑になり、描画に時間がかかります (たとえば、単一の色の描画は「小さく」なります。シャドウは多くの計算とレンダリングに「はるかにコストがかかります」)。
ブラウザによって実行される手順の簡単な概要を次に示します:
HTML マークアップを処理し、DOM ツリーを構築します。
CSS マークアップを処理し、CSSOM ツリーを構築します。
DOM と CSSOM をレンダー ツリーにマージします。
レンダリングツリーに従ってレイアウトし、各ノードの幾何情報を計算します。
各ノードを画面に描画します。
DOM または CSSOM が変更された場合は、画面上でどのピクセルを再レンダリングする必要があるかを決定するために、上記の手順をすべて再度実行する必要があります。
クリティカル レンダリング パスの最適化は、上記のシーケンスのステップ 1 から 5 の実行にかかる合計時間を最小限に抑えるプロセスです。これにより、コンテンツができるだけ早く画面にレンダリングされ、その間の時間も短縮されます。最初のレンダリング後に画面が更新されます。つまり、インタラクティブなコンテンツのリフレッシュ レートが向上します。
レンダリングをブロックする CSS デフォルトでは、CSS はレンダリングをブロックするリソースとして扱われます (ただし、 HTML の解析はブロックされません)。 )、これは、CSSOM が構築されるまで、ブラウザーは処理されたコンテンツをレンダリングしないことを意味します。 CSS を縮小し、できるだけ早く配信し、メディア タイプとクエリを活用してレンダリングのブロックを解除して、スクロールせずに見える範囲での時間を短縮してください。
レンダリング ツリーの構築では、DOM と CSSOM の両方がレンダリング ツリーの構築に必要です。これはパフォーマンスに重大な影響を与える可能性があります。HTML と CSS はどちらもレンダリングをブロックするリソースです。 DOM がなければレンダリングするものが何もないため、HTML は明らかに必要ですが、CSS の必要性はそれほど明白ではないかもしれません。 CSS レンダリングのブロックを行わずに通常の Web ページをレンダリングしようとするとどうなるでしょうか?
CSS はレンダリングをブロックするリソースです。最初のレンダリング時間を短縮するには、できるだけ早くクライアントにダウンロードする必要があります。
Web ページを表示するときや大型モニターに投影するときなど、特定の条件下でのみ使用される CSS スタイルがある場合はどうすればよいでしょうか?これらのリソースがレンダリングをブロックしなければ良いのですが。 そのような状況は、CSS の「メディア タイプ」と「メディア クエリ」によって解決できます。媒体查询由媒体类型以及零个或多个检查特定媒体特征状况的表达式组成。例如,第一个样式表声明未提供任何媒体类型或查询,因此它适用于所有情况。也就是说它始终会阻塞渲染。第二个样式表则不然,它只在打印内容时适用---或许您想重新安排布局、更改字体等等,因此在网页首次加载时,该样式表不需要阻塞渲染。最后一个样式表声明提供了由浏览器执行的“媒体查询”:符合条件时,样式表会生效,浏览器将阻塞渲染,直至样式表下载并处理完毕。
通过使用媒体查询,我们可以根据特定用例(比如显示或打印),也可以根据动态情况(比如屏幕方向变化、尺寸调整事件等)定制外观。声明样式表时,请密切注意媒体类型和查询,因为它们将严重影响关键渲染路径的性能。
让我们考虑下面这些实例:
第一个声明阻塞渲染,适用于所有情况。
第二个声明同样阻塞渲染:“all”是默认类型,和第一个声明实际上是等效的。
第三个声明具有动态媒体查询,将在网页加载时计算。根据网页加载时设备的方向,portrait.css 可能阻塞渲染,也可能不阻塞渲染。
最后一个声明只在打印网页时应用,因此网页在浏览器中加载时,不会阻塞渲染。
最后,“阻塞渲染”仅是指浏览器是否需要暂停网页的首次渲染,直至该资源准备就绪。无论媒寻是否命中,浏览器都会下载上述所有的CSS样式表,只不过不阻塞渲染的资源对当前媒体不生效罢了。
JavaScript 允许我们修改网页的方方面面:内容、样式以及它如何响应用户交互。不过,JavaScript 也会阻止 DOM 构建和延缓网页渲染。为了实现最佳性能,可以让 JavaScript 异步执行,并去除关键渲染路径中任何不必要的 JavaScript。
JavaScript 可以查询和修改 DOM 与 CSSOM。
JavaScript的 执行会阻止 CSSOM的构建,所以和CSSOM的构建是互斥的。
JavaScript blocks DOM construction unless explicitly declared as async.
JavaScript 是一种运行在浏览器中的动态语言,它允许对网页行为的几乎每一个方面进行修改:可以通过在 DOM 树中添加和移除元素来修改内容;可以修改每个元素的 CSSOM 属性;可以处理用户输入等等。为进行说明,让我们用一个简单的内联脚本对之前的“Hello World”示例进行扩展:
<html> <head><meta name="viewport" content="width=device-width,initial-scale=1"><link href="style.css?1.1.11" rel="stylesheet"><title>Critical Path: Script</title><style> body { font-size: 16px };p { font-weight: bold }; span { color: red };p span { display: none }; img { float: right }</style> </head> <body><p>Hello <span>web performance</span> students!</p><div><img src="awesome-photo.jpg"></div><script> var span = document.getElementsByTagName('span')[0]; span.textContent = 'interactive'; // change DOM text content span.style.display = 'inline'; // change CSSOM property // create a new element, style it, and append it to the DOM var loadTime = document.createElement('div'); loadTime.textContent = 'You loaded this page on: ' + new Date(); loadTime.style.color = 'blue'; document.body.appendChild(loadTime);</script> </body></html>
JavaScript 允许我们进入 DOM 并获取对隐藏的 span 节点的引用 -- 该节点可能未出现在渲染树中,却仍然存在于 DOM 内。然后,在获得引用后,就可以更改其文本,并将 display 样式属性从“none”替换为“inline”。现在,页面显示“Hello interactive students!”。
JavaScript 还允许我们在 DOM 中创建、样式化、追加和移除新元素。从技术上讲,整个页面可以是一个大的 JavaScript 文件,此文件逐一创建元素并对其进行样式化。但是在实践中,使用 HTML 和 CSS 要简单得多。
尽管 JavaScript 为我们带来了许多功能,不过也在页面渲染方式和时间方面施加了更多限制。
首先,请注意上例中的内联脚本靠近网页底部。为什么呢?如果我们将脚本移至 span元素前面,就会脚本运行失败,并提示在文档中找不到对任何span 元素的引用 -- 即 getElementsByTagName(‘span') 会返回 null。这透露出一个重要事实:脚本在文档的何处插入,就在何处执行。当 HTML 解析器遇到一个 script 标记时,它会暂停构建 DOM,将控制权移交给 JavaScript 引擎;等 JavaScript 引擎运行完毕,浏览器会从中断的地方恢复 DOM 构建。
换言之,我们的脚本块在运行时找不到网页中任何靠后的元素,因为它们尚未被处理!或者说:执行内联脚本会阻止 DOM 构建,也就延缓了首次渲染。
在网页中引入脚本的另一个微妙事实是,它们不仅可以读取和修改 DOM 属性,还可以读取和修改 CSSOM 属性。实际上,示例中就是这么做的:将 span 元素的 display 属性从 none 更改为 inline。最终结果如何?我们现在遇到了race condition(资源竞争)。
如果浏览器尚未完成 CSSOM 的下载和构建,而却想在此时运行脚本,会怎样?答案很简单,对性能不利:浏览器将延迟脚本执行和 DOM 构建,直至其完成 CSSOM 的下载和构建。
简言之,JavaScript 在 DOM、CSSOM 和 JavaScript 执行之间引入了大量新的依赖关系,从而可能导致浏览器在处理以及在屏幕上渲染网页时出现大幅延迟:
脚本在文档中的位置很重要。
当浏览器遇到一个 script 标记时,DOM 构建将暂停,直至脚本完成执行。
JavaScript 可以查询和修改 DOM 与 CSSOM。
JavaScript 执行将暂停,直至 CSSOM 就绪。即CSSDOM构建的优先级更高。
“优化关键渲染路径”在很大程度上是指了解和优化 HTML、CSS 和 JavaScript 之间的依赖关系谱。
默认情况下,JavaScript 执行会“阻塞解析器”:当浏览器遇到文档中的脚本时,它必须暂停 DOM 构建,将控制权移交给 JavaScript 运行时,让脚本执行完毕,然后再继续构建 DOM。实际上,内联脚本始终会阻止解析器,除非编写额外代码来推迟它们的执行。
通过 script 标签引入的脚本又怎样:
<html> <head><meta name="viewport" content="width=device-width,initial-scale=1"><link href="style.css?1.1.11" rel="stylesheet"><title>Critical Path: Script External</title> </head> <body><p>Hello <span>web performance</span> students!</p><div><img src="awesome-photo.jpg"></div><script src="app.js?1.1.11"></script> </body></html>
app.js
var span = document.getElementsByTagName('span')[0]; span.textContent = 'interactive'; // change DOM text contentspan.style.display = 'inline'; // change CSSOM property// create a new element, style it, and append it to the DOMvar loadTime = document.createElement('div'); loadTime.textContent = 'You loaded this page on: ' + new Date(); loadTime.style.color = 'blue'; document.body.appendChild(loadTime);
无论我们使用