这是我参与8月更文挑战的第4天,活动详情查看:8月更文挑战
简单说说浏览器的渲染过程
- 解析HTML,生成 DOM 树。
- 解析 CSS,生成 CSSOM(CSS 对象模型)。 大概是根据后代选择器之类的构建出类似 “字典树” 一样的结构。另外需要注意的是,浏览器会提供默认的样式(User Agent)作为兜底。
- DOM 树和 CSSOM 树进行合成,创建渲染树(render tree)。 具体做法是从 DOM 树根节点遍历可见节点,即排除设置了
display: none;
和不进行渲染(如<head>
)的节点。并对每个可见节点找到适配的 CSSOM 规则,需要考虑 “样式继承”(比如font-size
属性会影响子元素) 和 “样式层叠” (就是优先级,比如 ID 选择器比 类选择器优先级高)的情况。 - 【重排】进入 “布局”(Layout) 阶段,计算可见节点的几何信息。 计算元素的几何信息(如位置和尺寸)和样式信息(背景色之类)。比如
width: 50%;
这种相对值会根据父元素宽度得到绝对的像素值。 - 分层。文档中的层叠上下文(比如绝对定位的元素)和需要裁切的节点(比如在容器里放不下的文本)都会创建新的独立图层。
- 【重绘】绘制。或者叫栅格化,将渲染树得到的信息绘制到屏幕上。
PS:重排也叫回流。
重排
重排发生的条件
- 页面首次渲染
- 增删或替换可见的 DOM 元素
- 浏览器窗口发生变化
- 元素位置或尺寸发生变化
除了首次渲染,其他情况都对元素的的几何属性进行了修改,可能会对布局产生巨大的影响,所以需要对节点的几何信息等做重新的计算,也就是重排,然后绘制出来。
重排之后,都是要重绘的,流程就是这样。
不会触发重排的重绘的操作
- 对元素进行不会导致元素几何信息变化的操作。比如修改背景色、字体颜色。
如何减少重排重绘
因为重排很耗费性能,所以浏览器自己做了优化,会将多个修改操作缓存到队列里,在合适的时候再重排,也就说是异步的。但如果执行到 需要获取布局信息的代码(如 getBoundingClientRect,scrollHeight 之类的) 时,机会立即清空队列,进行重排。因为重新渲染的话,是拿不到最新的布局信息的。
减少重排的方法有:
- 减少 DOM 操作,使用合适的算法。 比如更新新增列表的一个项时,不要销毁掉所有的元素,再根据数组创建全新的列表项。React 和 Vue 的虚拟 DOM 的 Diff 算法优化就是减少了 DOM 的修改操作。
- 添加一个元素,先把它的子元素都创建好,再添加到 DOM 中。
- 合适地缓存布局信息。避免在循环中频繁获取布局信息,导致不断发生重排。
- 将经常发生变化的元素放在独立的一层。比如设置了
transform
的元素会使用独立的一个图层,会使用GPU
渲染,进行硬件加速,都不会引起重绘。我想可能是独立出来的一套动画系统。绝对定位这些脱离文档的方式,也会创建图层,只会在自身局部会发生回流重绘,能减少工作量。
JS 阻塞
首先了解下 script 标签的两个属性的功能:
defer
:在 DOM 树完成解析后再执行脚本;async
:脚本下载完后立即执行。无法保证 JS 脚本按顺序执行
阻塞的情况:
- JS 会阻塞 DOM 解析。 因为 JS 可能会操作 DOM,为了不做无用的工作,浏览器决定暂且搁置 DOM 的解析,先让脚本下载然后执行完再继续。思考一下,为什么我们建议把业务代码的 script 标签放在末尾?这是因为放在末尾的脚本能够等到 DOM 树解析完,业务代码往往需要访问节点元素,这样才不会拿到 null。至于一些第三方库不需要访问节点,所以放在开头并没有问题。
CSS 阻塞
要想构建 CSSOM 树,首先需要拿到 CSS 内容,而通常我们使用的都是外链的 CSS 文件。这样就会有一个请求的过程,于是阻塞发生了。
- CSS 阻塞不会影响 DOM 树的解析生成。(可通过
<script defer>
和一个延时返回的 CSS 资源来测试,defer
能够在 DOM 树完成解析后再执行脚本,结果会是先执行脚本,页面要晚一点才渲染出来 ) - **CSS 会阻塞页面渲染。**指的是阻塞 CSSOM 树的生成,因为信息不够完全,
- CSS 阻塞可能会阻塞后面的 JS 脚本的运行(其后存在没有设置
async
或defer
的 script 标签时)。效果就是CSS 阻塞 -> JS 阻塞 -> DOM 解析阻塞
,于是我们看到了 CSS 阻塞 DOM 解析 现象的发生。个人猜测原因是:浏览器认为脚本里可能有需要获取容器高度之类的内容,而这些必须要等页面渲染出来次能拿到,而标记了async
或defer
的脚本则不考虑这些。
Vue 首次加载白屏的问题
Vue 要构建 SPA(单页面应用),需要将所有的组件都提前放到 JS 脚本里,一次性下载。如果组件很多, JS 太大了,就会导致 DOM 渲染变慢,从而导致白屏加载速度较慢。
优化方案有:
合理地使用路由懒加载
下面是 vue-cli 脚手架启用路由选项后,给我们创建的路由配置。
{
path: '/about',
name: 'About',
// route level code-splitting
// this generates a separate chunk (about.[hash].js) for this route
// which is lazy-loaded when the route is visited.
// 看这里
component: () => import(/* webpackChunkName: "about" */ '../views/About.vue')
},
复制代码
About 组件只有在路由被访问时才会加载。也就是说,第一次访问首页或其他路由时,About 组件不会被加载。从而减少了首次加载 js 的大小,减少白屏时间。另外,注释里的 webpackChunkName
制定了加载的脚本的名称,如果多个懒加载组件都是用同一个 webpackChunkName
,那么它们会被放在同一个脚本里。也就是说,对于放在同一个脚本里的多个组件,当访问其中一个路由组件匹配的路由时,其他的组件也会提前下载好。
静态资源都使用 CDN 加速
CDN 通过缓存代理,将源站的资源分发在各个网络节点上,构造专用网络。这个专用网络是跨运营商、跨地域的,是真正的高速网络。通过负载均衡(利用 DNS),用户能够访问离它最近的资源,从而提高资源加载速度。相比跋山涉水地访问遥远的源节点,就近取水无疑速度更快。
这样 JS 脚本就能被快速加载,从而减少白屏时间。
这个算是提高加载速度的优化,我们还可以使用 http2、对脚本进行压缩、http 开启 gzip 压缩等老生常谈的减少资源大小和提高资源请求速度的方式。
分析代码,查看是否有没有复用的代码
尤其是用了多个版本的第三方库的情况。可以使用 webpack-bundle-analyzer 查看代码的依赖图。
先展示加载动画
屏幕一直白着也不好,弄些加载动画让用户知道网站是正常的,只是加载中而已,消除用户的不安。虽然不能优化速度,当能够告知用户网站正在的信息,可以提高用户体验。