🔥码云GVP开源项目 12k star Uniapp+ElementUI 功能强大 支持多语言、二开方便! 广告
为了解决性能的问题。 执行JS有一个JS引擎,那么执行渲染也有一个渲染引擎。 渲染引擎在不同的浏览器中也不是都相同的。 在**Firefox中叫做Gecko,在Chrome和Safari中都是基于WebKit开发的**。 学习关于WebKit的这部分渲染引擎内容。 ### 浏览器接受到HTML文件并转化为DOM树 当我们打开一个网页时,浏览器都会去请求对应的HTML文件。 在网络中传输的内容其实都是0和1这些字节数据。 当浏览器接收到这些字节数据以后,他会**将这些字节数据转为字符串**(我们的代码) ![](https://box.kancloud.cn/7bcfa616eab96e6e1b4e4bb98c359fab_311x83.png) 当数据转为字符串以后,浏览器会先**将这些字符串通过词法分析转为标记**(token),这一过程在词法分析中叫做标记化(tokenization) ![](https://box.kancloud.cn/fa6bb773e2db46597a3bfe6f203767d0_443x96.png) 标记(编译原理),标记还是字符串,是构成代码的最小单位。这一过程会将代码分拆成一块块,并给这些内容打上标记,便于理解这些最小单位的代码是什么意思。 ![](https://box.kancloud.cn/978f4aff9fff5329eca7052d0200b1e1_460x205.png) 当结束标记化后,这些**标记紧接着会准换为Node**,最后这些Node会根据不同Node之前的联系**构建为一颗DOM树**。 ![](https://box.kancloud.cn/703be5ff528c6a0d097aaffc99a344b9_521x266.png) 以上就是浏览器从网络中接收到HTML文件然后一系列的转换过程。 ![](https://box.kancloud.cn/bdf39c65a9d4b16cbb09a3bd33e3c140_619x114.png) 在解析HTML文件的时候,浏览器还会遇到CSS和JS文件,这时候浏览器也会去下载并解析这些文件 ### 将CSS文件转换为CSSOM树 转换CSS到CSSOM树的过程和上一节的过程类似 ![](https://box.kancloud.cn/d6a9a5cea96daca6d1234dcd9c53be55_664x104.png) 在这一个过程中,浏览器会确定下**每一个节点的样式到底是什么**,并且这一过程其实是很**消耗资源**的。 因为样式你可以**自行设置**给摸个节点,也可以通过**继承**获得。在这一过程中,浏览器得**递归CSSOM树**,然后确定具体的元素到底是什么样式。 ~~~ <div> <a> <span></span> </a> </div> <style> span { color: red; } div > a > span { color: red; } </style> ~~~ 分析为什么消耗资源: 第一种设置样式:浏览器只需要找到页面中所以span标签,然后设置颜色 第二种设置样式:浏览器先找到所有的span标签,再找到span标签上的a标签,最后再去找到div标签,然后给符合这种条件的span设置颜色。**递归的过程很复杂**。 所以: 我们应该尽可能的**避免写过于具体的CSS选择器**,然后读**HTML来说也尽量少的添加无意义的标签**,保证**层级扁平**。 ### 生成渲染树 当我们生成DOM树和CSSOM树以后,就需要将这两棵树**组合为渲染树**。 ![](https://box.kancloud.cn/e4af542e3a75b66c664e5522789ae5c3_686x352.png) 不是简单的将两者合并就行了,渲染树只会包括**需要显示的节点**和这些节点的样式信息,**`display:none`**,就不会在渲染树张显示。 当浏览器生成渲染树以后,就会根据渲染树来**进行布局**(也可以叫做**回流**),然后**调用GUP绘制,合成图层**,显示在屏幕上。 ### 为什么操作DOM慢 因为DOM是属于**渲染引擎**中东西,而JS又是**JS引擎**中的东西,当我们通过JS操作DOM的时候,其实这个操作涉及到了**两个线程之间的通信**,那么势必会带来一些**性能上的损耗**。操作DOM一多,也就等同于**一直在进行线程之间的通信**,并且操作DOM可能还会带来**重绘回流**的情况,所以也就导致了性能上的问题。 ### 插入几万个DOM,如何不卡顿? 首先我们肯定**不能一次把几万个DOM全部插入,这样肯定会造成卡顿**。 解决问题的重点:如何**分批次部分渲染DOM** 大部分人:通过**requestAnimationFrame的方式去循环的插入DOM**, 其他解决方案:虚拟滚动(virtualized scroller) **这种技术的原理就是只渲染可视区域内的内容,非可视区域的就完全不渲染了,当用户在滚动的时候,就去实时替换渲染的内容**。 ### 什么情况阻塞渲染 首先渲染的前提是生成渲染树,所以**HTML和CSS肯定会阻塞渲染**。如果你想渲染的越快,你越应该降低一开始需要渲染的文件大小,并且**扁平层级,优化选择器**。 当浏览器在**解析到script标签时,会暂停构建DOM**,完成后才会从暂停的地方重新开始。如果你想首屏渲染的越快,就越不应该在首屏就加载JS文件,这也是都建议**将script标签放在body标签底部的原因**。 并不是说script标签必须放在底部,也可以给**script标签添加defer或者async属性**。 当script标签加上defer属性以后,表示该**JS文件会并行下载**。但是会**放到HTML解析完成后顺序执行**,所以对于这种情况可以**把script标签放在任意位置**。 对于没有任何依赖的js文件,可**以加上async属性,表示JS文件下载和解析不会阻塞渲染**。 ## 重绘和回流 重绘和回流会在我们**设置节点样式时**频繁出现,同时也会很大程度上**影响性能**。 * 重绘时当节点需要**更改外观而不会影响布局**的,比如改变color就叫重绘。 * 回流是**布局或者几何属性需要改变**就称为回流。 * **回流必定发生重绘,重绘不一定引发回流**。 * 回流所需的成本比重绘高的多,改变父节点的子节点很可能会导致父节点一系列回流 以下几个动作可能会导致性能问题 * 改变window大小 * 改变字体 * 添加或删除样式 * 文字改变 * 定位或浮动 * 盒模型 重绘和回流其实也和EventLoop有关 1. 当Eventloop执行完Microtasks后,会判断document是否需要更新,因为浏**览器是60Hz的刷新率**,每16.6s才会更新一次 2. 然后判断是否有resize或者scroll事件,有的话回去触发事件,所以resize和scroll事件也是至少16ms才会触发一次,并且自带节流功能。 3. 判断是否触发了media query 4. 更新动画并且发送事件 5. 判断是否有全屏操作 6. 执行requestAnimationFrame回调 7. 执行IntersectionObserver回调,该方法用于判断元素是否可见,可以用于懒加载上,但兼容性不好 8. 更新界面 9. 以上就是每一帧中可能会做的事情,如果在一帧中有空闲时间,就回去执行**requestIdleCallback回调**。 requestAnimationFrame的回调会在每一帧确定执行,属于高优先级任务,而requestIdleCallback的回调则不一定,属于低优先级任务。 我们所看到的网页,都是浏览器一帧一帧绘制出来的,通常认为FPS为60的时候是比较流畅的,而FPS为个位数的时候就属于用户可以感知到的卡顿了 我们都知道React 16实现了新的调度策略(Fiber), 新的调度策略提到的异步、可中断,其实就是基于浏览器的 requestIdleCallback和requestAnimationFrame两个API。 requestIdleCallback回调的执行的前提条件是当前浏览器处于空闲状态。 ### 减少重绘和回流 1.** 使用transform替代top** ~~~ <div class="test"></div> <style> .test { position: absolute; top: 10px; width: 100px; height: 100px; background: red; } </style> <script> setTimeout(() => { // 引起回流 document.querySelector('.test').style.top = '100px' }, 1000) </script> ~~~ 2. **使用visibility替换display:none**,因为前者只会引起重绘,后者会引发回流(改变了布局) 3. 不要把节点的属性值放在一个循环里当成循环里的变量 ~~~ for(let i=0; i<1000; i++){ //获取offsetTop会导致回流,因为需要去获取正确的值 console.log(document.querySelector('.test').style.offsetTop) } ~~~ 4. 不要使用table布局,可能很小的一个小改动会造成整个table的重新布局 5. 动画实现的速度的选择,动画速度越快,回流次数越多,也可以选择使用requestAnimationFrame 6. CSS选择符**从右往左匹配查找**,避免节点层级过多 7. 将**频繁重绘或者回流的节点设置为图层**,图层能够阻止该节点的渲染行为影响别的节点,比如video标签来说,浏览器会自动将该节点变为图层 8. 设置节点为图层的方式有很多,我们可以通过以下常用属性可以生成新图层 * will-change * video、iframe标签 ## 思考题 在不考虑缓存和优化网络协议的前提下,考虑可以通过哪些方式来最快新人页面,也就是常说的**关键渲染路径**,这部分也是性能优化的一块内容 怎么测量到底有没有加快渲染速度呢? ![](https://box.kancloud.cn/808374ff35b705c681c207c55182c1f0_913x397.png) **当发生DOMContentLoaded事件后,就会生成渲染树**,生成渲染树就可以进行渲染了,这一过程更大程度上和硬件有关系了 ![](https://box.kancloud.cn/1e3179a273e764672566f2fbf7f7a9c7_988x592.png) 当文档中没有脚本时,浏览器解析完文档便能触发DOMContentLoaded事件;如果文档中包含脚本,则脚本会阻塞文档的解析,而脚本需要等位于脚本前面的css加载完才能执行。 任何情况下,DOMContentLoaded的触发**不需要等待图片等其他资源加载完成**。页面上所有的**资源(图片,音频,视频等)**被加载以后才会触发load事件,简单来说,页面的load事件会在DOMContentLoaded被触发之后才触发。 **提示如何加速**: 1. 从文件大小考虑 2. 从script标签使用上考虑 3. 从css,htmldiam书写上来考虑 4. 从需要下载的内容是否需要在首屏使用上来考虑