浏览器页面渲染和相关问题探讨

494 阅读7分钟

这是我参与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 脚本的运行(其后存在没有设置 asyncdefer 的 script 标签时)。效果就是 CSS 阻塞 -> JS 阻塞 -> DOM 解析阻塞,于是我们看到了 CSS 阻塞 DOM 解析 现象的发生。个人猜测原因是:浏览器认为脚本里可能有需要获取容器高度之类的内容,而这些必须要等页面渲染出来次能拿到,而标记了 asyncdefer 的脚本则不考虑这些。

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 查看代码的依赖图。

先展示加载动画

屏幕一直白着也不好,弄些加载动画让用户知道网站是正常的,只是加载中而已,消除用户的不安。虽然不能优化速度,当能够告知用户网站正在的信息,可以提高用户体验。

参考