关键渲染路径CRP笔记
关键渲染路径(Critical Render Process)是浏览器将HTML、CSS和JavaScript代码转换为屏幕上像素的步骤序列,它包含了DOM(Document Object Model)、CSSOM(CSS Object Model)、渲染树(Render Tree)和布局(Layout)。
浏览器的一次展示页面的过程从下载并解析HTML开始,将HTML解析为DOM,HTML内的<script>
标签可以外联JavaScript,JavaScript会反过来更改DOM。HTML内的<link>
标签又可以外联CSS,浏览器可以将CSS解析为CSSOM。浏览器引擎将DOM与CSSOM解析创建渲染树。之后与屏幕尺寸与像素有关的布局将被确定。最后浏览器的绘制引擎将会把布局绘制到页面上。
FOUC(Flash Of Unstyled Content)问题就是因为关键渲染路径某一步出问题导致的
关键渲染路径具体流程
浏览器使用渲染引擎或排版引擎(Browser Engine)来布局渲染以及DOM生成,常见渲染引擎有Safari、Chrome使用WebKit内的WebCore、Chrome除iOS端、Opera都在用的Blink(谷歌自行开发的WebCore分支)、Firefox用的Gecko。
Webkit集成了渲染引擎WebCore与JavaScript引擎JavaScirptCore,而Chrome中使用的Blink则仅为渲染引擎,V8为其JavaScript引擎。Firefox使用Gecko做渲染引擎,SpiderMonkey为其JavaScript引擎。
构建DOM树
浏览器接受HTML并处理为DOM的过程:Bytes → Characters → Tokens → Nodes → DOM。DOM树包含所有HTML标签包括注释与display:none
的内容,因为这一步还未关联到CSSOM上,这一步也包括JavaScript动态添加的内容。
当建立DOM时如果遇到了<script>
标签,由于JavaScript有可能会操作DOM树,所以构建DOM树的过程会分为以下三种相应方式
-
默认情况下DOM解析会停止,等待JavaScript完成下载(内联不需要)与执行然后继续解析
-
<script>
标签上有defer
属性,立即并行开始下载(运行环境负责并行任务,不影响DOM树解析),但是JavaScript的执行延迟到DOM树解析完后,DOMContentLoaded
之前(该事件原本在DOM树解析完成之后但事件会被推迟到代码执行之后),并且具有defer
标签的JS会按script
出现顺序顺序执行 -
<script>
标签上有async
属性,立即并行开始下载,下载完后停止DOM解析并执行JavaScript,执行顺序不一定按照script
出现顺序来
当构建DOM时遇到了CSS,CSS会被在另外的线程被下载执行,正常情况下不会影响DOM处理与JavaScript下载,但是任何JavaScript代码的执行必须在CSS下载与CSSOM构建完成之后,后面的渲染流程也必须在DOM和CSSOM都准备好之后再进行
构建CSSOM
浏览器处理CSS过程与处理HTML过程类似:Bytes → Characters → Tokens → Nodes → CSSOM,CSSOM(CSS Object Model)不包含不会被显示在页面上的HTML节点例如<meta>
、<script>
、<title>
等等。
只有DOM和CSSOM构建完毕后浏览器才会渲染元素,所以CSSOM的构建也会影响首屏时间。
渲染树构建
渲染树(Render Tree)包含了内容和样式也即是DOM和CSSOM,两者结合为渲染树,浏览器会从DOM树的根节点开始检查哪些CSS规则被添加,如果元素CSSOM上有display:none
则它本身和其后代都不会出现在渲染树中,渲染树中只包含可见元素。
布局Layout
布局(回流)在Chrome、Opera、Safari和Internet Explorer中被叫做Layout,在Firefox中叫做Reflow
布局决定了在哪里与如何在页面上放置元素,决定每个元素的宽和高,它取决于屏幕尺寸和像素数量。在首次布局完毕之后还有可能再次布局,当网站是响应式且用户改变浏览界面宽度比如手机的横屏竖屏调整时,或节点添加改变内容或者更新节点的盒模型时还会发生。
布局受DOM节点数量的影响,且布局操作会阻塞浏览器的其他操作,尽量减少布局次数
当执行以下操作时布局(Layout / Reflow)将会发生:
- 插入、删除、更新DOM元素
- 更改页面内容,例如修改Input中的文字
- 移动DOM元素
- DOM元素执行动画
- 更改CSS样式
- 更改一个元素的类名
class
- 更改窗口大小
- 滚动相关的属性与计算
- elem.scrollBy(), elem.scrollTo()
- elem.scrollIntoView(), elem.scrollIntoViewIfNeeded()
- elem.scrollWidth, elem.scrollHeight
- elem.scrollLeft, elem.scrollTop,设置它们的值,同样会影响
- 获取下列盒模型属性与计算时
- elem.offsetLeft, elem.offsetTop, elem.offsetWidth, elem.offsetHeight, elem.offsetParent
- elem.clientLeft, elem.clientTop, elem.clientWidth, elem.clientHeight
- elem.getClientRects(), elem.getBoundingClientRect()
- 聚焦方法
elem.focus()
会导致两次强制布局(回流) - 其他的
- elem.computedRole, elem.computedName
- elem.innerText
window.getComputedStyle()
- 要获取的元素在
shadow DOM
中 - 使用了
media queires
- min-width, min-height, max-width, max-height, width, height
- aspect-ratio, min-aspect-ratio, max-aspect-ratio
- device-pixel-ratio, resolution, orientation
- 获取以下某一种属性
- height, width
- top, right, bottom, left
- margin [-top, -right, -bottom, -left, 或简写] ,仅当 margin 是固定值。
- padding [-top, -right, -bottom, -left, 或简写] ,仅当 padding 是固定值。
- transform, transform-origin, perspective-origin
- translate, rotate, scale
- webkit-filter, backdrop-filter
- motion-path, motion-offset, motion-rotation
- x, y, rx, ry
- 要获取的元素在
- Window相关
- window.scrollX, window.scrollY
- window.innerHeight, window.innerWidth
- window.getMatchedCSSRules() 仅重新计算样式
- 表单
- inputElem.focus()
- inputElem.select(), textareaElem.select()
- 鼠标事件
- mouseEvt.layerX, mouseEvt.layerY, mouseEvt.offsetX, mouseEvt.offsetY
- Document
- doc.scrollingElement 仅重新计算样式
- Range
- range.getClientRects(), range.getBoundingClientRect()
- SVG
- 内容操作
更多的操作可以搜索Chromium的源码,关键词updateLayoutIgnorePendingStylesheets、updateLayoutTree
绘制Paint
绘制是将布局好的元素内容(例如边框、背景颜色、阴影、文本等)转化为屏幕上的像素的过程,绘制操作也被称为格栅化操作(Rasterization),浏览器在这一步也会为布局创建层Layer,绘制操作在浏览器的多个线程并行。
绘制操作以往是使用CPU来进行的但是这样会比较慢,现代浏览器支持使用GPU来进行绘制操作速度快。比较Chromium中软件和GPU绘制操作
绘制为每层的绘制,浏览器实际关键渲染路径最后还有一步合成(Composite),用于正确的将多层画面重叠显示在屏幕上
关键渲染路径性能分析
工具
Google提供了Lighthouse网站与Lighthouse NPM包分析工具用来对网站进行性能与可访问性的测试,能快速的测试、循环访问和分析CRP性能。
浏览器提供了dev tools调试工具,在Chrome Dev Tools的Network和Performance组件中可以分析页面性能
NodeJS提供了Inspector API,通过它我们可以用JavaScript代码与V8引擎的Inspector进行交互,可以分析CPU与堆的使用情况,Chrome Dev Tools实现了Inspector协议
浏览器提供了Navigation Timing API(IE与Safari不支持),通过这个API和页面加载时浏览器发出的其他事件可以捕获并记录任何页面的实际CRP性能
domLoading
:浏览器即将开始解析第一批收到的HTML文档字节domInteractive
:DOM准备就绪,浏览器完成对所有HTML的解析并且DOM构建完成domContentLoaded
:DOM和CSSOM均准备就绪,也就是DOM已准备就绪并且没有样式表阻止JavaScript的执行,现在可以构建渲染树了,大部分JavaScript引擎在该事件发生之后才开始执行JavaScript逻辑所以下方的两个时间戳可以帮我们记录花费的时间EventStart
EventEnd
domComplete
:所有处理完成并且网页上所有资源比如图像也已下载完毕,浏览器加载状态结束loadEvent
:浏览器触发onload
的事件触发额外的应用逻辑Start
End
关键渲染路径优化
像素管道
像素管道是一个对页面渲染每个像素的处理流程的一种抽象化描述,像素管道包含以下几个关键节点
- JavaScript:JavaScript实现的视觉效果,修改DOM、CSS Animations、Transitions、Web Animation API
- Style:计算CSS应用范围,具体标签选择器对应DOM节点
- Layout:计算布局,当页面元素位置或页面宽度元素宽度发生变化时会发生回流Reflow其实就是布局的重新计算
- Paint:填充像素,它涉及绘出文本、颜色、图像、边框和阴影,基本包括元素的每个可视部分,绘制是分布在不同层Layor上的
- Composite:合成,由于页面可能会分布在多层,合成的目的就是将所有的层按照顺序渲染出来
不是所有帧都会经过整个像素管道的处理,一般有三种情况:
-
JS/CSS → Style → Layout → Paint → Composite
如果修改元素的Layout属性,也就是改变了元素的几何属性例如宽度、高度、左侧或顶部位置、字体大小、增删DOM元素、浏览器大小变化、盒模型变化等,那么浏览器将必须检查所有的属性然后重排页面布局称作回流Reflow。发生回流的耗时和占用资源是最多的,因为浏览器会从
html
这个root frame
开始向下遍历依次计算所有节点的几何尺寸和位置,因为一个元素可能影响整个页面的布局。 -
JS/CSS → Style → Paint → Composite
如果只修改元素的Paint Only属性例如背景图片、文字颜色或阴影等,即不会影响页面布局的属性,浏览器将跳过布局,但是依然会执行绘制,称作重绘Repaint。回流Reflow(改变了Layout)之后也会重绘Repaint
-
JS/CSS → Style → Composite
如果更改了既不会导致回流,也不会造成重绘的操作,就会只进行样式计算和布局和合成操作,这种操作最轻量化,可以用在需要页面高性能的环境下例如动画或滚动场景。一个最重要的例子就是
transform
、perspective
、CSS样式的改变在Blink(Chrome的渲染引擎)和Gecko(Firefox的渲染引擎)中只会触发合成操作,开支非常小。
CSS Triggers网站提供了在这些CSS变化时会进行的以上三个流程的哪一个
优化建议
- 用
transform
、perspective
做形变和位移减少reflow
- 避免逐个修改节点样式尽量一次性修改
- 使用
DocumentFragment
需要多次追加元素或修改的内容缓存起来,最后一次性挂载到DOM上 - 需要多次修改的元素可以使用
display:none
先从DOM节点上移出,操作完再标记display:block
放回DOM上,因为不在DOM上的元素修改不会导致重绘 - 避免多次读取某些属性
- 使用
Flexbox
可以显著缩短布局耗费的时间 - 通过绝对定位将复杂的节点元素脱离文档流,形成新的Layer,降低回流成本
渲染优化建议
- 样式文件应该在
head
尽快下载执行(CSS下载解析在独立线程中不影响DOM下载解析)、JavaScript放在body
结束前(下载解析会阻塞DOM解析且JS有可能修改DOM元素,所以等到DOM内容基本完成解析再下载执行) - 优化CSS选择器,尽量减少层级嵌套,减少子选择等
- DOM的读写操作应该放在一起,读读读写写写但是不要读写读写读写
- 不要一条一条改变样式,尽量放到一个类中,或将元素脱离文档再修改样式
position
为absolute
或fixed
的元素重排开销较小,
总结
渲染流程
- 浏览器引擎边下载HTML边构建DOM树,以User Agent Stylesheet(浏览器内置样式)为原料构建CSSOM树
- 如果HTML解析到
<script>
- 默认下载与执行都阻塞DOM树的构建,
inline
的就阻塞并执行 defer
开另外线程下载但推迟执行到DOM树构建完毕且在DOMContentLoaded
之前async
开启另外线程下载但下载完立即阻塞DOM树构建并执行
- 默认下载与执行都阻塞DOM树的构建,
- 如果HTML解析到CSS,不论
inline
内联、internal
元素上还是外联的CSS(外联的需要下载完成之后)刷新CSSOM树,这个过程与DOM树生成是在不同线程不会阻塞DOM的生成- CSSOM中不会出现不可见的元素,包括
display:none
的元素以及其子元素,但visibility:hidden
或opacity:0
的元素还是会被添加到CSSOM上
- CSSOM中不会出现不可见的元素,包括
- 等到DOM树完成生成(JavaScript也执行完毕)、CSSOM树构建完成之后执行布局
- 布局完成之后执行绘制操作
性能优化
根据上方优化性能的操作编写代码与执行DOM操作
可能阻塞CRP的情况
HTML
如果HTML本身加载就很慢,就不用谈其他的了
CSS
CSS下载解析阻塞渲染树的创建与JavaScript的执行,但动态插入的外链CSS不会阻塞页面渲染,动态插入的内联CSS不会阻塞DOM的解析和渲染
JavaScript
同步的JavaScript会阻塞DOM的解析和渲染,异步的JavaScriptdefer
和async
在下载时都不会影响DOM的生成,但async
会在下载完成之后执行时阻塞DOM生成,动态插入的外链JavaScript脚本不会阻塞DOM解析生成