前面介绍多个前端性能优化的点,现在我们来说一下性能优化的最后一关——页面渲染
浏览器渲染的过程我们之前也稍微提过,这里再说一下解析 HTML 文件,构建 DOM 树,同时浏览器主进程负责下载 CSS 文件
CSS 文件下载完成,解析 CSS 文件成树形的数据结构,然后结合 DOM 树合并成 RenderObject 树
布局 RenderObject 树 (Layout/reflow),负责 RenderObject 树中的元素的尺寸,位置等计算
绘制 RenderObject 树 (paint),绘制页面的像素信息
浏览器主进程将默认的图层和复合图层交给 GPU 进程,GPU 进程再将各个图层合成(composite),最后显示出页面
重绘(Repaint)
当页面中元素样式的改变并不影响它在文档流中的位置时(例如:color、background-color、visibility 等),浏览器会将新样式赋予给元素并重新绘制它,这个过程称为重绘。
回流(Reflow)
当 Render Tree 中部分或全部元素的尺寸、结构、或某些属性发生改变时,浏览器重新渲染部分或全部文档的过程称为回流。 会导致回流的操作:
- 页面首次渲染
- 浏览器窗口大小发生改变
- 元素尺寸或位置发生改变元素内容变化(文字数量或图片大小等等)
- 元素字体大小变化
- 添加或者删除可见的 DOM 元素
- 激活 CSS 伪类(例如:hover)
- 查询某些属性或调用某些方法
- 一些常用且会导致回流的属性和方法,因为这些API需要实时获取,所以会触发回流 clientWidth、clientHeight、clientTop、clientLeftoffsetWidth、offsetHeight、offsetTop、offsetLeftscrollWidth、scrollHeight、scrollTop、scrollLeftscrollIntoView()、scrollIntoViewIfNeeded()、getComputedStyle()、getBoundingClientRect()、scrollTo()
回流比重绘的代价要更高。有时即使仅仅回流一个单一的元素,它的父元素以及任何跟随它的元素也会产生回流。现代浏览器会对频繁的回流或重绘操作进行优化:浏览器会维护一个队列,把所有引起回流和重绘的操作放入队列中,如果队列中的任务数量或者时间间隔达到一个阈值的,浏览器就会将队列清空,进行一次批处理,这样可以把多次回流和重绘变成一次。
或者我们可以通过“离线操作”的方式来操作DOM
let container = document.getElementById('container') container.style.display = 'none' container.style.width = '100px' container.style.height = '200px' container.style.border = '10px solid red' container.style.color = 'red' ...(省略了许多类似的后续操作) container.style.display = 'block' 复制代码
再有就是css选择器,css选择器每一次遍历都的过程是非常消耗性能的,应该减小那些没有必要的选择器堆叠,比如#container li{}
虽然看起来搜索范围很小,从具体的id选择器中找li元素,但是实际上在选择元素时是从右向左寻找的,会先找所有的li,然后再找#container下面的,不如直接设置类选择器。
JS 引擎是独立于渲染引擎存在的,我们在第一节也说过,当HTML文档中引入script脚本时会阻塞浏览器解析DOM和CSSOM,脚本执行完成之后才会继续解析,对此我们可已使用异步defer或者async脚本
<script async src="index.js"></script> <script defer src="index.js"></script> 复制代码
async 模式下,JS 不会阻塞浏览器做任何其它的事情。它的加载是异步的,当它加载结束,JS 脚本会立即执行,defer 模式下,JS 的加载是异步的,执行是被推迟的。等整个文档解析完成、DOMContentLoaded 事件即将被触发时,被标记了 defer 的 JS 文件才会开始执行。
服务端渲染
服务端渲染是在服务端将首屏的HTML文档渲染好之后在发送给客户端,解决了客户端加载首屏过长的问题,我们现在绝大多数的webApp都是客户端渲染,HTML文档中只有一个根节点,往往是这个样子的
<body> <div id="app"></div> </body> 复制代码
只有一个根节点,根据后期的js运行来渲染页面内容,这就需要等待js全部加载完成才能开始渲染,这就导致了首屏时间过长,所以出现了服务端渲染的技术来解决首屏加载时间长的问题,服务端渲染并不是javaweb那种每切换一次网页都在服务端获取一次HTML,而是通过第一册请求的url将首屏的HTML文档直接填充内容,客户端在获取资源之后直接渲染就是了,之后切换路由也不需要再去请求HTML文件,只需要ajax交互就行了
具体的实现细节可以看我之前的SSR实现
服务端渲染固然能带来用户体验的提升,但是牺牲的同样是服务器的计算资源,服务端渲染本质上是本该浏览器做的事情,分担给服务器去做,当服务器顶不住压力时,这又是一波“反向优化”