avatar
fireworks99
keep hungry keep foolish

关于浏览器渲染网页的细节

一个网站通常遇到的问题有:资源加载缓慢、首次渲染时等待不重要文件的下载、无样式内容的闪烁等等。为了避免这些问题,我们需要了解浏览器渲染一个典型网页的生命周期。

DOM(Document Object Model)

  1. 浏览器阅读HTML代码的时候,每遇到一个HTML element(诸如htmlbodydiv等等)都会创建一个(被称为Node的)JavaScript对象(即该对象是Node类的实例),最终所有的HTML element被转换为JavaScript对象。

    • 不同的HTML element对应的Node对象创建自不同的类:
      • div => HTMLDivElement => HTMLElement => Element => Node => EventTarget
      • script => HTMLScriptElement => HTMLElement => Element => Node => EventTarget
  2. 从HTML文档创建了node objects后,浏览器用这些node objects创建出一个树形结构,称为DOM Tree。

    • DOM node 不一定非得是 HTML element,像comments、attribute、text(注释、属性、文本)虽然不是HTML element,但它们是DOM Tree 上独立的node。

      (补充:HTML tag 里的 attribute 对应 node object 的 properties,例如<body id='1'>something</body>document.body.id='2'

  3. JavaScript不认识DOM,因为DOM并不属于JS规范,而是high-level Web API

CSSOM(Cascading SS OM)

  1. style priority:external/embedded/inline(developer) > user agent stylesheet(browser) > W3C CSS standard(W3C document)
  2. DOM 中,那些不会被打印到屏幕上的 elements,如<meta><script><title>等,不会出现了CSSOM里。但display:none的elements出现在CSSOM里,不出现在Render Tree里。

Render Tree

  1. DOM Tree + CSSOM Tree => Render Tree. 渲染树建立之前不会有任何东西被打印到屏幕上。
  2. DOM Tree 是 incrementally 更新的,CSSOM Tree 是按“单”(sheet)更新的,Tender Tree 是随 DOM Tree incrementally 更新而 incrementally 更新的。
  3. DOM Tree 与 CSSOM Tree 的建立是并发的。因为共用主线程,所以不并行

Critical Rendering Path

  1. 网页被加载,浏览器读取HTML创建DOM Tree,处理CSS(inline/embedded/external)创建CSSOM Tree,合成Render Tree。
  2. 针对Render Tree上每一个独立节点,创建layout(size of each node in pixels and position)。
  3. 为每一个elements (or a sub-tree) in the Render-Tree 创建一个图层,首先,用不同的线程在各自的图层里paint。这个过程称为光栅化rasterization
  4. 将所有的图层组合(composit)起来,绘制在屏幕上。绘制的时候,每个图层又被分成许多片(tiles),可以保证reflow与repaint更高效。

critical rendering path

HTML parsing

  1. 浏览器请求网页,服务器收到请求,开始发送HTML文本。浏览器收到文档中的前几行后就开始解析HTML,伴随着解析,逐步地(incrementally)建立DOM Tree,每次向DOM Tree 添加一个node。

    这样不需要非得等整个HTML文档加载完毕再解析,提高效率。

    如果网络很慢的话,第一次收到的HTML文本解析完,创建完DOM Tree、Render Tree,绘制在屏幕上,此时可能还未收到第二份数据。

    即 HTML parsing 可以按字节处理(process content one byte at a time)。

CSS parsing

  1. CSS parsing 不像 HTML parsing 那样可以按字节处理,因为后面的规则可能覆盖前面的规则。如果像 HTML parsing 那样处理,会频繁地发生reflow与repaint,尤其是因为CSS是级联的(cascading),影响显得更大。
  2. 每次解析完一整个 stylesheet 才去更新 CSSOM Tree。CSSOM Tree 一旦更新完毕,随即 Render Tree 更新完毕, 随即渲染在屏幕上。
  3. 像“外部资源的请求”、“JS与用户交互”等事件是明显耗时的,HTML parsing 要等待这些事件所以称为“被阻塞”。而纵使CSS parsing 与 HTML parsing共用主线程,CSS parsing时,HTML parsing需要等待其完成,但这一过程由CPU在一瞬间完成,无所谓“等待”,称不上“阻塞”。(阻塞通常是ms级的或肉眼可见的等待)
  4. 外部CSS的加载(load/download)会阻塞Render Tree的建立,从关键渲染路径来看,纵使此时DOM Tree仍在更新,但Render Tree的更新停止了,自然不会有后续的layout与paint,此时屏幕上不会有任何东西被打印,直至CSS加载完。加载完后瞬间解析完成Render Tree的更新,计算layout,完成paint。
  5. 浏览器遇到内部CSS(embedded / inline),会立即解析,完成Render Tree的更新,计算layout,完成paint。
  6. script可以使用ansyc、defer属性来避免阻塞HTML parsing,CSS可以设置media来避免阻塞渲染,像media='print'浏览器可以等待打印页面时再加载该css。

summary

  1. 在遇到一个新的CSS之前,Render Tree 随 DOM Tree 逐步(incremental)更新而更新,并且计算layout而后完成paint(由此可见paint也是incremental的,只不过大部分网站努力做了性能优化,使这一过程难以靠肉眼分辨)。
  2. 某时刻遇到一个新的CSS,其加载阻塞了Render Tree的更新,即阻塞了关键渲染路径,此时页面停止paint。
  3. CSS加载完成时,瞬间解析,更新Render Tree,计算layout,完成paint,发生Flash of Unstyled Content(FOUC),一定有repaint,可能有reflow。这即是建议将CSS前置于head标签内的原因(避免FOUC)。
  4. 遇到两个连续的script时,按理说应该等第一个执行完再下载第二个,但是浏览器做了优化,允许同时下载多个script,但是仍然保证执行顺序是出现顺序而非加载完成的顺序。

DOMContentLoaded

  1. DOM Tree构建完成时触发DOMContentLoaded事件,此时外部资源(诸如CSS、image)可能还未加载完。但是,大部分浏览器做了优化,他们等外部CSS加载完解析完即CSSOM Tree构建完才去触发DOMContentLoaded事件。
  2. 普通script(ansyc / defer不普通)会阻塞HTML parsing,自然延迟DOMContentLoaded事件的触发。而CSS阻塞普通script,也间接延迟了DOMContentLoaded事件的触发。

Performance metrics

FP(First Paint)

FCP(First Contentful Paint)

FMP(first meaningful paint)

LCP(Largest Contentful Paint)

DLC(DOMContentLoaded)

Site by Baole Zhao | Powered by Hexo | theme PreciousJoy