浏览器的底层渲染机制
引言
故事从一道经典面试题开始,从浏览器的地址栏输入https://www.baidu.com,到用户看到界面,这个过程到底都发生了什么?
答:URL解析->缓存检查->DNS解析->TCP三次握手->数据传输->TCP四次挥手->页面渲染
浏览器是如何渲染页面的
前置概念
基础知识1、什么是渲染
在网页开发中,渲染指的是将代码 ( HTML
CSS
JavaScript
) 转化为用户可交互的一些页面,该过程一般是由浏览器渲染引擎来完成的。简单来说,渲染就是浏览器将代码变成人眼看到图像的全过程。
基础知识2、浏览器的组成
在浏览器中打开一个网页相当于新起了一个进程。浏览器的核心由五个线程组成:GUI渲染线程、JS引擎线程、事件触发线程、定时触发器线程、异步http请求线程。
注意:GUI 渲染线程与 JS 引擎线程是互斥的,如果JS执行的时间过长,这样就会造成页面的渲染不连贯,导致页面渲染加载阻塞。
开始解析
【DOM树】
- 浏览器从磁盘或网络读取HTML的原始字节,并根据文件的指定编码(例如 UTF-8)将它们转换成字符串。
- 将字符串转换成Token,例如:
<html>
、<body>
等。Token中会标识出当前Token是“开始标签”或是“结束标签”亦或是“文本”等信息 - 生成节点对象并构建DOM
注意:DOM树的生成过程中可能会被CSS和JS的加载执行阻塞。渲染阻塞问题下文会讲。
【CSSOM树】
浏览器解析CSS文件并生成CSS规则树,每个CSS文件都被分析成一个StyleSheet对象
注意:CSS匹配HTML元素是一个相当复杂和有性能问题的事情。所以,DOM树要小,CSS尽量用id和class,千万不要过渡层叠下去
【Render-Tree渲染树】
通过DOM树和CSS规则树我们便可以构建渲染树。浏览器会先从DOM树的根节点开始遍历每个可见节点。对每个可见节点,找到其适配的CSS样式规则并应用。
注意:渲染树只会包括需要显示的节点和这些节点的样式信息,如果某个节点是 display: none
的,那么就不会在渲染树中显示。
问题:如果在渲染的过程中遇到<script></script>,DOM 构建将暂停,直至脚本完成执行,然后继续构建DOM。所以,script 标签的位置很重要。
CSS 优先:引入顺序上,CSS 资源先于 JavaScript 资源。
JS置后:我们通常把JS代码放到页面底部,且JavaScript 应尽量少影响 DOM 的构建。并不是说 script 标签必须放在底部,可以给 script 标签添加 defer 或者 async 属性。
当浏览器生成渲染树以后,就会根据渲染树来进行布局(也可以叫做回流)。这一阶段浏览器要做的事情是要弄清楚各个节点在页面中的确切位置和大小。通常这一行为也被称为“自动重排”。
重绘是指当浏览器在渲染页面时,发现某个元素的样式发生了变化,需要重新绘制元素的内容到页面上的过程。
总结步骤:
-
处理 HTML 标记,构建 DOM 树
-
处理 CSS 标记,构建 CSSOM 树
-
将 DOM 树和 CSSOM 树融合成渲染树
-
根据生成的渲染树,计算它们在设备视口(viewport)内的确切位置和大小,这个计算的阶段就是回流 => 布局(Layout)或 重排(reflow)
-
根据渲染树以及回流得到的几何信息,得到节点的绝对像素 => 绘制(painting)
CRP渲染优化方案
基于浏览器的渲染过程,后来就有人提出针对性的优化方案,关键渲染路径优化(Critical Rendering Path)。简单来说就是基于这个五个步骤去左优化。
-
标签语义化和避免深层次嵌套
-
CSS选择器渲染是从右到左
-
尽早尽快地把CSS下载到客户端(充分利用HTTP多请求并发机制)
-
style
-
link
-
@import。使用@import引用的css文件只有在引用它的那个css文件被下载, 解析之后, 浏览器才知道还有另外一个css需要下载,这时候才开始去下载并解析, 构建render tree等一系列操作, 这就导致浏览器无法并行下载所需的样式文件。
-
放到顶部
-
-
避免阻塞的JS加载
-
async
-
defer
-
放到底部
-
- 减少DOM的回流和重绘
回流和重绘
概念
为什么要避免回流?回流是一项非常昂贵的操作,因为它涉及到整个渲染树的重新计算和布局,会消耗大量的计算资源和时间,尽可能减少回流的次数,是提高网页性能的重要手段之一。回流一定触发重绘,重绘不一定触发回流。
重绘:元素的样式改变,但是宽高、大小、位置不变。如:outline,background-color,color,visibility等
回流:元素的大小、位置发生变化(页面的布局和几何信息发生变化的时候)触发了重新布局,导致渲染树重新计算布局和渲染。如添加或删除可见的dom元素,元素的位置发生变化,元素的尺寸发生变化,元素内容发生变化(文本变化或图片被另外一个不同尺寸的图片替代);页面一开始渲染的时候(不能避免);因为回流是根据视口的大小来计算元素的位置和大小的,所以浏览器的窗口尺寸变化也会引发回流...
如何减少回流
- 放弃传动操作dom的时代,基于vue/react开启数据影响视图模式。mvvm/mvc/virtual dom/dom diff
- 分离读写操作(现代的浏览器都有渲染队列的机制)
offsetTop、offsetLeft、offsetWidth、offsetHeight、clientTop、clientLeft、clientWidth、clientHeight、scrollTop、scrollLeft、scrollWidth、scrollHeight、getComputedStyle、currentStyle...都会刷新渲染队列
- 样式集中改变
div.style.cssText = 'width:20px;height:20px';
div.className = 'box';
- 缓存布局信息
div.style.left = div.offsetLeft + 1 + 'px'; div.style.top= div.offsetTop + 1 + 'px'; =>改为 var curLeft = div.offsetLeft; var curTop = div.offsetTop; div.style.left = curLeft + 1 + 'px'; div.style.top = curTop + 1 + 'px';
- 元素批量修改。文档碎片:createDocumentFragment、模板字符串拼接
- 动画效果应用到position属性为absolute或fixed的元素上(脱离文档流)
- css硬件加速(GPU加速)
比起考虑如何减少回流和重绘,我们更期望的是根本不要回流和重绘;transform\opacity\filters...这些属性都会触发硬件加速,不会引发回流和重绘...
可能会引发的坑:过多使用会占用大量内存,性能消耗严重,有时候会导致字体模糊等。
- 牺牲平滑度换取速度
每次1像素移动一个动画,但是如果此动画使用了100%CPU,动画看上去是跳动的,因为浏览器正在与更新回流做斗争,每次移动3像素可能看起来平滑度低了,但它不会导致CPU在较慢的机器中抖动
- 避免使用table布局和使用css的javascript表达式