彻底理解浏览器的渲染原理

242 阅读5分钟

一个网页从输入URL地址到显示是一个怎样的过程?

  • 输入地址后,地址通过DNS服务器进行域名解析出对应的ip地址

  • ip地址对应的主机发送http请求获取对应的静态资源

  • 默认情况下服务器会返回index.html文件

index.html是怎么被解析渲染的?

一个网页下载下来后由渲染引擎进行解析,下面内容都是由浏览器内核(比如苹果的webkit内核的WebCore部分和谷歌的blink)进行解析渲染的,详细图解如下:

Snipaste_2024-07-16_17-41-04.png

1. HTML解析

浏览器内核解析HTML会构建DOM Tree

Snipaste_2024-07-16_17-59-54.png

2. 生成CSS规则
  • 在解析过程中当遇到link元素引入CSS文件时,浏览器会下载CSS文件

  • 下载CSS文件不会影响HTML的解析,是和DOM Tree的生成同时进行的

  • 下完后会对CSS文件进行解析,解析出对应的规则树,即CSSOMCSS Object Model CSS对象模型)

Snipaste_2024-07-17_09-22-46.png

3. 构建Render Tree
  • DOM TreeCSSOM Tree结合构建Render Tree

  • 因此link元素不会阻塞DOM Tree的构建,但是会阻塞Render Tree的构建

  • Render TreeDOM Tree并不是一一对应的,比如DOM Tree中设置display:none的元素就不会出现在Render Tree

  • Render Tree 仅包含需要显示的可见元素,并应用了CSS样式,但是不表示每个节点的尺寸、位置等信息

Snipaste_2024-07-17_09-29-54.png

4. 布局(Layout

布局生成渲染树中所有节点的宽度、高度和位置信息,结果存储在一个或多个框架或盒子对象中

回流

第一次确定节点的大小位置称为布局(Layout) ,之后对节点大小位置改变后的重新计算称为回流(reflow),也可称重排

什么情况下会引起回流?
总结一句话就是,只要操作会改变元素大小和布局都会引起回流,回流一定会引起重绘

  • DOM结构发生改变(添加新的节点或者移除节点)
  • 改变布局(修改了width、height、padding、font-size等值)
  • 修改浏览器窗口的尺寸
  • 调用getComputedStyle获取大小位置信息本身不会直接引起回流,但为了返回准确的样式信息,有的浏览器可能会执行回流以确保数据的准确性

如何减少回流?

  • 修改样式时尽量一次性修改,比如通过cssText和添加class修改

  • 尽量避免频繁的操作DOM,可以在一个DocumentFragment或者父元素中将要操作的DOM操作完成,再一次性的操作

  • 对于频繁变化的元素,使用position: absoluteposition: fixed以避免影响其他元素的布局

  • 尽量避免频繁读取会导致回流的属性

5. 绘制(Paint

在绘制阶段,浏览器将布局阶段计算的每个frame(也称为渲染对象或渲染盒子)转为屏幕上实际的像素点,包括将每个元素的视觉特性(颜色、边框、阴影等)绘制到多个图层上

重绘

第一次对元素的颜色背景阴影等的渲染叫绘制,之后当元素背景色、文字颜色、边框颜色、样式外观等属性发生变化时的再次渲染叫重绘

6. 合成(composite

Snipaste_2024-07-17_13-56-12.png

  • 将图层组合成最终的屏幕图像

  • 绘制过程中会绘制到多个合成图层中,这是浏览器的一种优化手段

  • 默认情况下,标准流中的内容都是被绘制在同一个图层(Layer)中的

  • 一些特殊的属性会创建一个新的合成层(CompositingLayer),并且新的图层可以利用GPU来加速绘制,因为每个合成层都是单独渲染的

  • 分层确实可以提高性能,但是它以内存管理为代价,因此不应作为web性能优化策略的一部分过度使用

常见的一些特殊的属性如下:
调试时可以在浏览器更多工具中的layer里看到以下属性是否会生成新的图层

  • 3D transforms

  • video、canvas、iframe

  • opacity 动画转换时;

  • position: fixed

  • will-change:一个实验性的属性,提前告诉浏览器元素可能发生哪些变化;

  • animationtransition 设置了opacity、transform

JavaScript脚本和页面解析的关系

  • 浏览器在解析HTML的过程中,遇到script元素会停止构建DOM Tree

  • 然后会下载JavaScript代码并执行JavaScript脚本

  • JavaScript脚本执行完后才会继续解析HTML,构建DOM Tree

  • 这是因为JavaScript作用之一就是操作和修改DOM,若等DOM Tree构建和渲染之后再执行JavaScript,会造成严重的回流和重绘,影响页面性能

若想让JavaScript代码的下载不阻塞构建DOM Tree,则需要用到下面script元素的两个属性

defer属性
<!DOCTYPE html>
<html>
<head>
  <title>Defer Example</title>
  <script src="first.js" defer></script>
  <script src="second.js" defer></script>
</head>
<body>
  <h1>Hello, world!</h1>
  <script>
    document.addEventListener('DOMContentLoaded', function() {
      console.log('DOMContentLoaded event triggered');
    });
  </script>
</body>
</html>
  • defer 属性告诉浏览器不要等待脚本下载,继续解析HTML,构建DOM Tree

  • 如果所有带有 defer 属性的脚本都已经提前下载好了,它会等待DOM Tree构建完成,然后触发 DOMContentLoaded事件

  • 确保了当 DOMContentLoaded 事件触发时,整个 DOM Tree已经构建完成,所有依赖的脚本都已经执行,可以安全地进行 DOM 操作

  • 多个带defer的脚本是可以保持正确的顺序执行的

  • defer可以提高页面的性能,并且推荐放到head元素中

  • defer仅适用于外部脚本

async属性
  • async属性也不会阻塞解析HTML

  • async脚本不能保证顺序,它是独立下载、独立运行,不会等待其他脚本

  • async不能保证在DOMContentLoaded之前或者之后执行;

  • async通常用于独立的脚本,对其他脚本和DOM没有依赖的