一覧WB-13-1 · 描画とページの性能

リソースの読み込みとレンダリング

HTML・CSS・画像を読み込み、Resource Timingの取得記録を確認します。同じ箱の高さ・色・transform・表示状態を順に変更し、境界の座標と周囲の配置を測ります。rectはgetBoundingClientRectとoffset系の値を返し、座標は小数2桁へ丸めます。

説明

ブラウザはHTMLからDOMを作り、CSSを適用して要素の寸法や位置を決め、描画します。取得する資源と、変更によって必要になる計算は別の観点です。見える位置が変わっただけで、周囲の配置まで変わったとは限りません。

HTML・CSS・画像を読み込み、Resource Timingの取得記録を確認します。同じ箱の高さ・色・transform・表示状態を順に変更し、境界の座標と周囲の配置を測ります。rectはgetBoundingClientRectとoffset系の値を返し、座標は小数2桁へ丸めます。

1HTML・CSS・画像の読込み

外部CSSと画像を持つ文書を読み込みます。

(globalThis.doc=await loadDocument("/render")資源を含む文書の読込みを待つ,{ready:doc.readyState,styles:doc.styleSheets.lengthスタイルシート数,image:{complete:doc.querySelector("img").complete,width:doc.querySelector("img").naturalWidth画像自体の幅,height:doc.querySelector("img").naturalHeight}})

文書の読込みが完了し、スタイルシート一件と200×100の画像を確認できます。loadDocumentはiframeを作り、そのloadを待ってDocumentを返す関数です。画像の取得完了と、その表示領域の大きさは別に確認できます。

2取得の開始元と完了を確認する

文書内のResource Timing記録を表示します。

doc.defaultView.performance.getEntriesByType("resource")文書が取得した資源記録.map(e=>({path:new URL(e.name).pathname,initiator:e.initiatorType取得を開始した種類,completed:e.responseEnd>=e.startTime,bodyBytes:e.decodedBodySize資源本文のバイト数})).sort((a,b)=>a.path.localeCompare(b.path))

画像はimg、CSSはlinkを開始元として記録されます。decodedBodySizeは復号後の資源本文のバイト数で、通信全体のヘッダーやTLSなどを含む値ではありません。資源の取得記録だけから、その後のレイアウトや描画の全コストは分かりません。

3DOMにあるが箱を作らない要素

hiddenの段落がDOMに残ることと、表示寸法を確認します。

({exists:Boolean(doc.getElementById("hidden")DOM上の存在),display:doc.defaultView.getComputedStyle適用後のスタイル(doc.getElementById("hidden")).display,rect:rect("#hidden")表示寸法})

要素は存在しますがdisplayはnoneで、境界寸法は0です。DOMにあるすべての要素が表示上の箱を持つわけではありません。

4変更前の配置

箱と後続段落の位置を保存します。

(globalThis.box=doc.getElementById("box"),globalThis.base=rect("#following")比較用に後続位置を保存,{box:rect("#box")箱の寸法,following:base})

箱の高さ40pxの後ろに段落が配置されています。次の操作では箱だけの指定を変え、後続の位置に影響するか比較します。

5高さ変更で後続の位置が変わる

箱の高さを100pxにします。

(box.style.height="100px"通常配置の高さを変更,{box:rect("#box"),following:rect("#following"),move:Math.round((rect("#following").y-base.y後続位置の差)*100)/100})

高さの差60pxほど、後続の段落も下へ移ります。この変更は通常の配置寸法に影響します。寸法を読むAPIは、必要なスタイルやレイアウトの更新を反映した値を返します。

6色の変更では寸法は変わらない

背景色だけを変更します。

(globalThis.before=rect("#box"),box.style.backgroundColor色だけを変更="rgb(120, 40, 90)",{color:doc.defaultView.getComputedStyle(box).backgroundColor,sameRect:座標と寸法の一致JSON.stringify(before)===JSON.stringify(rect("#box"))})

色は変わりますが、測定した座標と寸法は同じです。色を描き直す必要と、周囲の配置を計算し直す必要は分けて考えられます。内部のpaint回数はこの座標測定とは別の記録で確認する対象です。

7transformによる表示位置の変更

箱を右へ30px、下へ10px移動します。

(globalThis.beforeTransform=rect("#box"),globalThis.beforeFollowing=rect("#following"),box.style.transform="translate(30px, 10px)"配置を保って表示を移動,{before:beforeTransform,after:rect("#box")境界とoffsetを比較,followingSame:後続の位置を比較JSON.stringify(beforeFollowing)===JSON.stringify(rect("#following"))})

境界座標は右30px・下10pxほど変わりますが、offsetTop/offsetLeftと後続段落の位置は変わりません。transformは通常の配置後の見え方へ作用します。常にGPUだけで処理されるかどうかは、このAPIの結果だけでは決まりません。

8不透明度を変える

opacityを0.5にします。

(globalThis.beforeOpacity=rect("#box"),box.style.opacity="0.5"半透明へ変更,{opacity:doc.defaultView.getComputedStyle(box).opacity,sameRect:寸法の維持を確認JSON.stringify(beforeOpacity)===JSON.stringify(rect("#box"))})

座標と寸法は同じまま、半透明になります。重ね合わせの結果は変わりますが、通常配置の寸法を変える指定ではありません。

9visibilityでは場所が残る

箱をvisibility:hiddenにします。

(box.style.visibility="hidden"配置を残して非表示,{box:rect("#box"),following:rect("#following")})

箱は見えなくなっても、寸法と後続段落の場所は残ります。非表示にする指定にも、配置を残すものと取り除くものがあります。

10display:noneでは後続が詰まる

displayをnoneへ変えます。

(box.style.display="none"箱を作らない指定,{box:rect("#box"),following:rect("#following")後続位置の変化})

箱の境界寸法は0になり、後続段落が上へ移ります。DOMから要素を削除したわけではなく、表示の箱を作らない指定になっています。色・transform・opacity・displayを一括して同じ種類の変更として扱わず、何が変わるかを測って判断します。

まとめ

Resource Timingは取得した資源の情報であり、描画全体の処理時間とは別です。

高さの変更は周囲の配置を変え、transformは通常配置を保って見える位置を変えます。

色・不透明度・非表示の各指定について、寸法と見え方の変化を分けて確認します。

操作履歴と解説を振り返れます。

ブラウザ / 描画・実行記録

1
(globalThis.doc=await loadDocument("/render"),{ready:doc.readyState,styles:doc.styleSheets.length,image:{complete:doc.querySelector("img").complete,width:doc.querySelector("img").naturalWidth,height:doc.querySelector("img").naturalHeight}})
{
  "ready": "complete",
  "styles": 1,
  "image": {
    "complete": true,
    "width": 200,
    "height": 100
  }
}
2
doc.defaultView.performance.getEntriesByType("resource").map(e=>({path:new URL(e.name).pathname,initiator:e.initiatorType,completed:e.responseEnd>=e.startTime,bodyBytes:e.decodedBodySize})).sort((a,b)=>a.path.localeCompare(b.path))
[
  {
    "path": "/asset.svg",
    "initiator": "img",
    "completed": true,
    "bodyBytes": 204
  },
  {
    "path": "/style.css",
    "initiator": "link",
    "completed": true,
    "bodyBytes": 161
  }
]
3
({exists:Boolean(doc.getElementById("hidden")),display:doc.defaultView.getComputedStyle(doc.getElementById("hidden")).display,rect:rect("#hidden")})
{
  "exists": true,
  "display": "none",
  "rect": {
    "x": 0,
    "y": 0,
    "width": 0,
    "height": 0,
    "offsetLeft": 0,
    "offsetTop": 0,
    "offsetWidth": 0,
    "offsetHeight": 0
  }
}
4
(globalThis.box=doc.getElementById("box"),globalThis.base=rect("#following"),{box:rect("#box"),following:base})
{
  "box": {
    "x": 16,
    "y": 66.87,
    "width": 200,
    "height": 40,
    "offsetLeft": 16,
    "offsetTop": 67,
    "offsetWidth": 200,
    "offsetHeight": 40
  },
  "following": {
    "x": 16,
    "y": 114.86,
    "width": 607.93,
    "height": 24,
    "offsetLeft": 16,
    "offsetTop": 115,
    "offsetWidth": 608,
    "offsetHeight": 24
  }
}
5
(box.style.height="100px",{box:rect("#box"),following:rect("#following"),move:Math.round((rect("#following").y-base.y)*100)/100})
{
  "box": {
    "x": 16,
    "y": 66.87,
    "width": 200,
    "height": 100,
    "offsetLeft": 16,
    "offsetTop": 67,
    "offsetWidth": 200,
    "offsetHeight": 100
  },
  "following": {
    "x": 16,
    "y": 174.86,
    "width": 607.93,
    "height": 24,
    "offsetLeft": 16,
    "offsetTop": 175,
    "offsetWidth": 608,
    "offsetHeight": 24
  },
  "move": 60
}
6
(globalThis.before=rect("#box"),box.style.backgroundColor="rgb(120, 40, 90)",{color:doc.defaultView.getComputedStyle(box).backgroundColor,sameRect:JSON.stringify(before)===JSON.stringify(rect("#box"))})
{
  "color": "rgb(120, 40, 90)",
  "sameRect": true
}
7
(globalThis.beforeTransform=rect("#box"),globalThis.beforeFollowing=rect("#following"),box.style.transform="translate(30px, 10px)",{before:beforeTransform,after:rect("#box"),followingSame:JSON.stringify(beforeFollowing)===JSON.stringify(rect("#following"))})
{
  "before": {
    "x": 16,
    "y": 66.87,
    "width": 200,
    "height": 100,
    "offsetLeft": 16,
    "offsetTop": 67,
    "offsetWidth": 200,
    "offsetHeight": 100
  },
  "after": {
    "x": 46,
    "y": 76.87,
    "width": 200,
    "height": 100,
    "offsetLeft": 16,
    "offsetTop": 67,
    "offsetWidth": 200,
    "offsetHeight": 100
  },
  "followingSame": true
}
8
(globalThis.beforeOpacity=rect("#box"),box.style.opacity="0.5",{opacity:doc.defaultView.getComputedStyle(box).opacity,sameRect:JSON.stringify(beforeOpacity)===JSON.stringify(rect("#box"))})
{
  "opacity": "0.5",
  "sameRect": true
}
9
(box.style.visibility="hidden",{box:rect("#box"),following:rect("#following")})
{
  "box": {
    "x": 46,
    "y": 76.87,
    "width": 200,
    "height": 100,
    "offsetLeft": 16,
    "offsetTop": 67,
    "offsetWidth": 200,
    "offsetHeight": 100
  },
  "following": {
    "x": 16,
    "y": 174.86,
    "width": 607.93,
    "height": 24,
    "offsetLeft": 16,
    "offsetTop": 175,
    "offsetWidth": 608,
    "offsetHeight": 24
  }
}
10
(box.style.display="none",{box:rect("#box"),following:rect("#following")})
{
  "box": {
    "x": 0,
    "y": 0,
    "width": 0,
    "height": 0,
    "offsetLeft": 0,
    "offsetTop": 0,
    "offsetWidth": 0,
    "offsetHeight": 0
  },
  "following": {
    "x": 16,
    "y": 66.87,
    "width": 607.93,
    "height": 24,
    "offsetLeft": 16,
    "offsetTop": 67,
    "offsetWidth": 608,
    "offsetHeight": 24
  }
}

演習完了