Web ページの表示は、次のようなステップを経ると考えることができます。
もちろん、この記事ではコンポジット部分のみに焦点を当てます。
コンポジットについて説明する前に、その後の概念の理解を容易にするために、一部のブラウザ (この記事は Chrome のみを対象としています) のレンダリング原理を簡単に理解する必要があります。詳細については、「Chrome での GPU アクセラレーションによる合成」を参照してください
注: Blank エンジンの一部の実装に対する Chrome の変更により、RenderObject が LayoutObject になるなど、私たちが慣れ親しんだクラス名の一部が変更されています。 RenderLayer が PaintLayer になります。興味があれば、Slimming Paint をチェックしてください。
ブラウザでは、ページのコンテンツは Node オブジェクトで構成されるツリー構造、つまり DOM ツリーとして保存されます。各 HTML 要素にはそれに対応する Node オブジェクトがあり、DOM ツリーのルート ノードは常にドキュメント ノードになります。これについては誰もがよく知っていると思いますが、実際には、DOM ツリーから最終レンダリングまでに何らかの変換マッピングが必要です。
DOM ツリー内の各 Node ノードには、対応する LayoutObject があります。 LayoutObject は、ノードのコンテンツを画面上に描画する方法を知っています。
一般に、同じ座標空間を持つ LayoutObject は同じレンダリング レイヤ (PaintLayer) に属します。 PaintLayer はもともと、ページ要素が正しい順序で合成されるようにするためのスタッキング コンテスト (スタッキング コンテキスト) を実装するために使用され、重なり合う要素や半透明の要素などが正しく表示されるようにしました。したがって、カスケード コンテキストを形成するための条件を満たす LayoutObject は、必ず新しいレンダリング レイヤーを作成します。もちろん、オーバーフロー != を持つ要素など、一部の特殊な LayoutObject に対して新しいレンダリング レイヤーが作成される特殊なケースもあります。見える。 PaintLayer を作成する理由に応じて、一般的な 3 つのカテゴリに分類できます:
NormalPaintLayer
OverflowClipPaintLayer
NoPaintLayer
上記の条件を満たす LayoutObject は独立したレンダリング レイヤーを持ちますが、他の LayoutObject はレンダリング レイヤーを持つ最初の親要素と 1 つのレンダリング レイヤーを共有します。
一部の特殊なレンダリング レイヤは合成レイヤ (合成レイヤ) とみなされますが、合成レイヤではない他のレンダリング レイヤは他のレンダリングと結合されます。レイヤーは、GraphicsLayer と 1 つの親レイヤーを共有します。
各 GraphicsLayer には GraphicsContext があり、GraphicsContext はこのレイヤーのビットマップを出力し、最後に GPU が複数のビットマップを合成します。この時点で、私たちのページが画面に表示されます。
レンダリング レイヤがコンポジション レイヤにプロモートされる理由はいくつかあります。
注: レンダリング レイヤをコンポジション レイヤにプロモートするには前提条件があります。レンダリング レイヤは SelfPaintingLayer である必要があります。 (基本的には記事で紹介した上記のNormalPaintLayerとみなしてください)。以下で説明するレンダリング レイヤがコンポジション レイヤに昇格される状況はすべて、レンダリング レイヤが SelfPaintingLayer であるという前提に基づいています。
直接的な理由
3D またはハードウェア アクセラレーションされた 2D キャンバス要素
フラッシュなどのハードウェア アクセラレーション プラグイン
不透明度、変換、フィルター、背景フィルターにアニメーションまたはトランジションを適用します (アクティブなアニメーションまたはトランジションである必要があります。アニメーションまたはトランジション効果が適用されるとき)開始または終了していません。合成レイヤーのリフティングも失敗します)
デモ: トランジション
will-change は、不透明度、変換、上、左、下、右に設定されます (上、左などは、相対などの明確な位置属性を設定する必要があります)。デモ
子孫要素の理由
オーバーラップ オーバーラップの理由
オーバーラップにより複合レイヤーが生成される理由?シンプルな栗を贈ります。
青い四角形は緑の四角形と重なっており、その親要素は GraphicsLayer です。このとき、緑色の四角形は GraphicsLayer であると想定されます。オーバーラップで合成レイヤーを昇格できない場合、青色の四角形は合成レイヤーに昇格せず、親要素と GraphicsLayer を共有します。
このとき、レンダリング順序が間違ってしまいます。そのため、レンダリング順序を確保するために、合成レイヤーの重なりも原因となります。これが通常の状況です。
もちろん、重複する理由もいくつかのカテゴリに細分化されます。次にそれらを詳しく見ていきます。
複合レイヤーをオーバーラップまたは部分的にオーバーラップします。
では、どのようにオーバーラップとしてカウントされるのでしょうか? 最も一般的でわかりやすいのは、要素の境界ボックス (コンテンツ + パディング + ボーダー) がコンポジション レイヤーと重なっていることです。 もちろん、デモです。マージン領域の重なりは無効です(デモ)。他にも、次のような一般的ではない状況があり、これらはコンポジション レイヤーとのオーバーラップの条件と見なすことができます。
想定オーバーラップ。
この理由は少し間違っているように思えますが、仮説の重複とは何ですか?実際、アニメーション中に要素の CSS アニメーション効果が他の要素と重なる場合などは、より理解しやすいでしょう。この状況に対応して、assumeOverlap の合成レイヤーには理由があります。デモを参照してください。このデモでは、アニメーション化された要素はその兄弟要素と視覚的に重なりませんが、assumeOverlap により、その兄弟要素は引き続きコンポジション レイヤーにプロモートされます。
この理由により、非常に特殊な状況が存在することに注意してください:
コンポジション レイヤーにインライン変換属性がある場合、その兄弟レンダリング レイヤーがオーバーラップしているとみなされるため、構成層に昇格します。例: デモ。
基本的に、一部の複合レイヤーを改善する一般的な理由は、オーバーラップによるものであることがわかります。その理由は、多数の合成レイヤーが無造作に生成される可能性があり、各合成レイヤーが CPU とメモリ リソースを消費し、ページのパフォーマンスに重大な影響を与えるためです。ブラウザもこれを考慮して、レイヤー圧縮(Layer Squashing)処理を備えています。複数のレンダリング レイヤーが同じコンポジション レイヤーとオーバーラップする場合、オーバーラップによる「レイヤー爆発」の可能性を防ぐために、これらのレンダリング レイヤーは GraphicsLayer に圧縮されます。詳細については、以下のデモをご覧ください。最初は、青い正方形が
translationZ によってコンポジション レイヤーに昇格されました。他の正方形要素はオーバーラップにより一緒に圧縮され、そのサイズはこれら 3 つの正方形を含む長方形のサイズになりました。
緑色の四角形にマウスを置くと、translateZ 属性が設定され、緑色の四角形が合成レイヤーに昇格され、残りの 2 つは一緒に圧縮されます。 . サイズは、これら 2 つの正方形を含む長方形のサイズに縮小されます。
もちろん、ブラウザーの自動レイヤー圧縮は万能ではありません。以下に示すように、ブラウザーがレイヤー圧縮を実行できない特定の状況も多くあります。可能な限り避けてください。 (注: 以下の状況はすべて重複する理由に基づいています)
レンダリング順序を壊す圧縮を実行できません (squashingWouldBreakPaintOrder)
例は次のとおりです:
<style> #ancestor { -webkit-mask-image: -webkit-linear-gradient(rgba(0,0,0,1), rgba(0,0,0,0)); } #composited { width: 100%; height: 100%; transform: translateZ(0); } #container { position: relative; width: 400px; height: 60px; border: 1px solid black; } #overlap-child { position: absolute; left: 0; top: 0 ; bottom: 0px; width: 100%; height: 60px; background-color: orange; }</style>
<div id="container"> <div id="composited">Text behind the orange box.</div> <div id="ancestor"> <div id="overlap-child"></div> </div></div>
この場合、 `#overlap- child` はコンポジションレイヤーと同じであり、圧縮を行うと描画順序が変更され、その親要素 `#ancestor` のマスク属性が無効になるため、このような状況ではレイヤー圧縮を実行できません。現在、この問題を引き起こす一般的な状況は 2 つあります。1 つは、上記のマスク属性を使用する祖先要素であり、もう 1 つは、filter 属性を使用する祖先要素です ([demo](http://taabaofed.github.io/)。デモ/パフォーマンス-コンポジット-デモ/squash/squashingWouldBreakPaintOrder-filter.html))。
ビデオ要素のレンダリング レイヤは圧縮できず、他のレンダリング レイヤはビデオが配置されているコンポジション レイヤに圧縮できません (squashingVideoIsDisallowed) デモ
iframe とプラグインのレンダリング レイヤは圧縮できません。また、他のレンダリング レイヤは、それらが配置されているコンポジション レイヤに圧縮できません (squashingLayoutPartIsDisallowed) デモ
リフレクションを使用したレンダリングレイヤー (squashingReflectionDisallowed) デモ
ブレンド モード属性でレンダリング レイヤを圧縮できません (squashingBlendingDisallowed) デモ
レイヤがコンポジション レイヤと異なります。 クリッピング コンテナを使用する場合、レンダリング レイヤを圧縮できません (squashingClippingContainerMismatch)。
例は次のとおりです: デモ
<style> .clipping-container { overflow: hidden; height: 10px; background-color: blue; } .composited { transform: translateZ(0); height: 10px; background-color: red; } .target { position:absolute; top: 0px; height:100px; width:100px; background-color: green; color: #fff; }</style>
<div class="clipping-container"> <div class="composited"></div></div><div class="target">不会被压缩到 composited div 上</div>
この例では、`.target` は合成レイヤー `.composited` と重なっていますが、`.composited` は `overflow:hidden` コンテナ内にあるため、`.target` と合成レイヤーは異なるクリッピングを持ちます。コンテナであるため、「.target」を圧縮することはできません。
コンポジション レイヤに対して相対的にスクロールするレンダー レイヤは圧縮できません (scrollsWithRespectToSquashingLayer)
例は次のとおりです。 >
<style> body { height: 1500px; overflow-x: hidden; } .composited { width: 50px; height: 50px; background-color: red; position: absolute; left: 50px; top: 400px; transform: translateZ(0); } .overlap { width: 200px; height: 200px; background-color: green; position: fixed; left: 0px; top: 0px; }</style>
この例では、赤色の `.composited` が複合レイヤーに昇格され、緑色の `.overlap` 修正 ページの上部には、最初は `.composited` コンポジション レイヤーのみがあります。 ![](https://img.alicdn.com/tps/TB1SHBOMXXXXXbnXFXXXXXXXXXX-690-484.jpg_640x640.jpg) ページをスライドして `.overlap` が `.composited` に重なると、原因は合成レイヤーに昇格しており、合成レイヤーに対して相対的にスクロールするため圧縮できません。 ![](https://img.alicdn.com/tps/TB1IrRGMXXXXXXxaXXXXXXXXXXXX-690-484.jpg_640x640.jpg)
<div class="composited"></div><div class="overlap"></div>
当渲染层同合成层有不同的具有 opacity 的祖先层(一个设置了 opacity 且小于 1,一个没有设置 opacity,也算是不同)时,该渲染层无法压缩(squashingOpacityAncestorMismatch,同 squashingClippingContainerMismatch) demo
当渲染层同合成层有不同的具有 transform 的祖先层时,该渲染层无法压缩(squashingTransformAncestorMismatch,同上) demo
当渲染层同合成层有不同的具有 filter 的祖先层时,该渲染层无法压缩(squashingFilterAncestorMismatch,同上) demo
当覆盖的合成层正在运行动画时,该渲染层无法压缩(squashingLayerIsAnimating),当动画未开始或者运行完毕以后,该渲染层才可以被压缩 demo
使用 Chrome DevTools 工具来查看页面中合成层的情况。
比较简单的方法是打开 DevTools,勾选上 Show layer borders
其中,页面上的合成层会用黄色边框框出来。
当然,更加详细的信息可以通过 Timeline 来查看。
每一个单独的帧,看到每个帧的渲染细节:
点击之后,你就会在视图中看到一个新的选项卡:Layers。
点击这个 Layers 选项卡,你会看到一个新的视图。在这个视图中,你可以对这一帧中的所有合成层进行扫描、缩放等操作,同时还能看到每个渲染层被创建的原因。
有了这个视图,你就能知道页面中到底有多少个合成层。如果你在对页面滚动或渐变效果的性能分析中发现 Composite 过程耗费了太多时间,那么你可以从这个视图里看到页面中有多少个渲染层,它们为何被创建,从而对合成层的数量进行优化。
提升为合成层简单说来有以下几点好处:
利用合成层对于提升页面性能方面有很大的作用,因此我们也总结了一下几点优化建议。
合成层的好处是不会影响到其他元素的绘制,因此,为了减少动画元素对其他元素的影响,从而减少 paint,我们需要把动画效果中的元素提升为合成层。
提升合成层的最好方式是使用 CSS 的 will-change 属性。从上一节合成层产生原因中,可以知道 will-change 设置为 opacity、transform、top、left、bottom、right 可以将元素提升为合成层。
#target { will-change: transform;}
其兼容如下所示:
对于那些目前还不支持 will-change 属性的浏览器,目前常用的是使用一个 3D transform 属性来强制提升为合成层:
#target { transform: translateZ(0);}
但需要注意的是,不要创建太多的渲染层。因为每创建一个新的渲染层,就意味着新的内存分配和更复杂的层的管理。之后我们会详细讨论。
如果你已经把一个元素放到一个新的合成层里,那么可以使用 Timeline 来确认这么做是否真的改进了渲染性能。别盲目提升合成层,一定要分析其实际性能表现。
文章最开始,我们讲到了页面呈现出来所经历的渲染流水线,其实从性能方面考虑,最理想的渲染流水线是没有布局和绘制环节的,只需要做合成层的合并即可:
为了实现上述效果,就需要只使用那些仅触发 Composite 的属性。目前,只有两个属性是满足这个条件的:transforms 和 opacity。更详细的信息可以查看 CSS Triggers 。
注意:元素提升为合成层后,transform 和 opacity 才不会触发 paint,如果不是合成层,则其依然会触发 paint。具体见如下两个 demo。
demo 1:transform
demo 2:opacity
可以看到未提升 target element 为合成层,transform 和 opacity 依然会触发 paint。
对于不需要重新绘制的区域应尽量避免绘制,以减少绘制区域,比如一个 fix 在页面顶部的固定不变的导航 header,在页面内容某个区域 repaint 时,整个屏幕包括 fix 的 header 也会被重绘,见 demo ,结果如下:
而对于固定不变的区域,我们期望其并不会被重绘,因此可以通过之前的方法,将其提升为独立的合成层。
减少绘制区域,需要仔细分析页面,区分绘制区域,减少重绘区域甚至避免重绘。
看完上面的文章,你会发现提升合成层会达到更好的性能。这看上去非常诱人,但是问题是,创建一个新的合成层并不是免费的,它得消耗额外的内存和管理资源。实际上,在内存资源有限的设备上,合成层带来的性能改善,可能远远赶不上过多合成层开销给页面性能带来的负面影响。同时,由于每个渲染层的纹理都需要上传到 GPU 处理,因此我们还需要考虑 CPU 和 GPU 之间的带宽问题、以及有多大内存供 GPU 处理这些纹理的问题。
对于合成层占用内存的问题,我们简单做了几个 demo 进行了验证。
demo 1 和 demo 2 中,会创建 2000 个同样的 div 元素,不同的是 demo 2 中的元素通过 will-change 都提升为了合成层,而两个 demo 页面的内存消耗却有很明显的差别。
通过之前的介绍,我们知道同合成层重叠也会使元素提升为合成层,虽然有浏览器的层压缩机制,但是也有很多无法进行压缩的情况。也就是说除了我们显式的声明的合成层,还可能由于重叠原因不经意间产生一些不在预期的合成层,极端一点可能会产生大量的额外合成层,出现层爆炸的现象。我们简单写了一个极端点但其实在我们的页面中比较常见的 demo 。
<style> @-webkit-keyframes slide { from { transform: none; } to { transform: translateX(100px); } } .animating { width: 300px; height: 30px; background-color: orange; color: #fff; -webkit-animation: slide 5s alternate linear infinite; } ul { padding: 5px; border: 1px solid #000; } .box { width: 600px; height: 30px; margin-bottom: 5px; background-color: blue; color: #fff; position: relative; /* 会导致无法压缩:squashingClippingContainerMismatch */ overflow: hidden; } .inner { position: absolute; top: 2px; left: 2px; font-size: 16px; line-height: 16px; padding: 2px; margin: 0; background-color: green; }</style>
<!-- 动画合成层 --><div class="animating">composited animating</div><ul> <!-- assume overlap --> <li class="box"> <!-- assume overlap --> <p class="inner">asume overlap, 因为 squashingClippingContainerMismatch 无法压缩</p> </li> ...</ul>
demo 中, .animating 的合成层在运行动画,会导致 .inner 元素因为上文介绍过的 assumedOverlap 的原因,而被提升为合成层,同时, .inner 的父元素 .box 设置了 overflow: hidden ,导致 .inner 的合成层因为 squashingClippingContainerMismatch 的原因,无法压缩,就出现了层爆炸的问题。
这种情况平时在我们的业务中还是很常见的,比如 slider + list 的结构,一旦满足了无法进行层压缩的情况,就很容易出现层爆炸的问题。
解决层爆炸的问题,最佳方案是打破 overlap 的条件,也就是说让其他元素不要和合成层元素重叠。对于上述的示例,我们可以将 .animation 的 z-index 提高。修改后 demo
.animating { ... /* 让其他元素不和合成层重叠 */ position: relative; z-index: 1;}
此时,就只有 .animating 提升为合成层,如下:
同时,内存占用比起之前也降低了很多。
如果受限于视觉需要等因素,其他元素必须要覆盖在合成层之上,那应该尽量避免无法层压缩情况的出现。针对上述示例中,无法层压缩的情况(squashingClippingContainerMismatch),我们可以将 .box 的 overflow: hidden 去掉,这样就可以利用浏览器的层压缩了。修改后 demo
此时,由于第一个 .box 因为 squashingLayerIsAnimating 的原因无法压缩,其他的都被压缩到了一起。
同时,内存占用比起之前也降低了很多。
之前无线开发时,大多数人都很喜欢使用 translateZ(0) 来进行所谓的硬件加速,以提升性能,但是性能优化并没有所谓的“银弹”, translateZ(0) 不是,本文列出的优化建议也不是。抛开了对页面的具体分析,任何的性能优化都是站不住脚的,盲目的使用一些优化措施,结果可能会适得其反。因此切实的去分析页面的实际性能表现,不断的改进测试,才是正确的优化途径。