This commit is contained in:
louzefeng
2024-07-11 05:50:32 +00:00
parent bf99793fd0
commit d3828a7aee
6071 changed files with 0 additions and 0 deletions

View File

@@ -0,0 +1,163 @@
<audio id="audio" title="21 | Chrome开发者工具利用网络面板做性能分析" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/03/f9/030227dfbb99951536220bbf68fa43f9.mp3"></audio>
“浏览器中的页面循环系统”模块我们已经介绍完了,循环系统是页面的基础,理解了循环系统能让我们从本质上更好地理解页面的工作方式,加深我们对一些前端概念的理解。
接下来我们就要进入新的模块了也就是“浏览器中的页面”模块正如专栏简介中所言页面是浏览器的核心浏览器中的所有功能点都是服务于页面的而Chrome开发者工具又是工程师调试页面的核心工具所以在这个模块的开篇我想先带你来深入了解下Chrome开发者工具。
**Chrome开发者工具简称DevTools是一组网页制作和调试的工具内嵌于Google Chrome 浏览器中**。Chrome开发者工具非常重要所蕴含的内容也是非常多的熟练使用它能让你更加深入地了解浏览器内部工作原理。Chrome开发者工具也在不停地迭代改进如果你想使用最新版本可以使用[Chrome Canary](https://www.google.com/intl/en/chrome/canary/)。)
作为这一模块的第一篇文章,我们主要聚焦**页面的源头**和**网络数据的接收**这些发送和接收的数据都能体现在开发者工具的网络面板上。不过为了你能更好地理解和掌握我们会先对Chrome开发者工具做一个大致的介绍然后再深入剖析网络面板。
## Chrome开发者工具
Chrome开发者工具有很多重要的面板比如与性能相关的有网络面板、Performance面板、内存面板等与调试页面相关的有Elements面板、Sources面板、Console面板等。
你可以在浏览器窗口的右上方选择Chrome菜单然后选择“更多工具&gt;开发者工具”来打开Chrome开发者工具。打开的页面如下图所示
<img src="https://static001.geekbang.org/resource/image/68/8d/68edf7b09e33b5481b49dc76967b838d.png" alt="">
从图中可以看出它一共包含了10个功能面板包括了Elements、Console、Sources、NetWork、Performance、Memory、Application、Security、Audits和Layers。
关于这10个面板的大致功能我做了一个表格感兴趣的话你可以详细看下
<img src="https://static001.geekbang.org/resource/image/c5/82/c5eb9603e79547ae3d815254e24d4782.png" alt="">
简单来说Chrome开发者工具为我们提供了通过界面访问或者编辑DOM和CSSOM的能力还提供了强大的调试功能和查看性能指标的能力。
OK接下来我们就要重点看下其中重要的Network面板即网络面板。
## 网络面板
网络面板由控制器、过滤器、抓图信息、时间线、详细列表和下载信息概要这6个区域构成如下图所示
<img src="https://static001.geekbang.org/resource/image/46/57/46fba54f54b9bd43918308f9f1ae1357.png" alt="">
### 1. 控制器
其中控制器有4个比较重要的功能我们按照下文中的这张图来简单介绍下。
<img src="https://static001.geekbang.org/resource/image/f0/42/f02477088c0499247e0ed37f46ad2a42.png" alt="">
- 红色圆点的按钮,表示“开始/暂停抓包”,这个功能很常见,很容易理解。
- “全局搜索”按钮,这个功能就非常重要了,可以在所有下载资源中搜索相关内容,还可以快速定位到某几个你想要的文件上。
- Disable cache即“禁止从Cache中加载资源”的功能它在调试Web应用的时候非常有用因为开启了Cache会影响到网络性能测试的结果。
- Online按钮是“模拟2G/3G”功能它可以限制带宽模拟弱网情况下页面的展现情况然后你就可以根据实际展示情况来动态调整策略以便让Web应用更加适用于这些弱网。
### 2. 过滤器
网络面板中的过滤器主要就是起过滤功能。因为有时候一个页面有太多内容在详细列表区域中展示了而你可能只想查看JavaScript文件或者CSS文件这时候就可以通过过滤器模块来筛选你想要的文件类型。
### 3. 抓图信息
抓图信息区域可以用来分析用户等待页面加载时间内所看到的内容分析用户实际的体验情况。比如如果页面加载1秒多之后屏幕截图还是白屏状态这时候就需要分析是网络还是代码的问题了。勾选面板上的“Capture screenshots”即可启用屏幕截图。
### 4. 时间线
时间线主要用来展示HTTP、HTTPS、WebSocket加载的状态和时间的一个关系用于直观感受页面的加载过程。如果是多条竖线堆叠在一起那说明这些资源被同时被加载。至于具体到每个文件的加载信息还需要用到下面要讲的详细列表。
### 5. 详细列表
这个区域是最重要的,它详细记录了每个资源从发起请求到完成请求这中间所有过程的状态,以及最终请求完成的数据信息。通过该列表,你就能很容易地去诊断一些网络问题。
详细列表是我们本篇文章介绍的重点,不过内容比较多,所以放到最后去专门介绍了。
### 6. 下载信息概要
下载信息概要中你要重点关注下DOMContentLoaded和Load两个事件以及这两个事件的完成时间。
- DOMContentLoaded这个事件发生后说明页面已经构建好DOM了这意味着构建DOM所需要的HTML文件、JavaScript文件、CSS文件都已经下载完成了。
- Load说明浏览器已经加载了所有的资源图像、样式表等
通过下载信息概要面板,你可以查看触发这两个事件所花费的时间。
## 网络面板中的详细列表
下面我们就来重点介绍网络面板中的详细列表,这里面包含了大量有用的信息。
### 1. 列表的属性
列表的属性比较多比如Name、Status、Type、Initiator等等这个不难理解。当然你还可以通过点击右键的下拉菜单来添加其他属性这里我就不再赘述了你可以自己上手实操一下。
另外,你也可以按照列表的属性来给列表排序,默认情况下,列表是按请求发起的时间来排序的,最早发起请求的资源在顶部。当然也可以按照返回状态码、请求类型、请求时长、内容大小等基础属性排序,只需点击相应属性即可。
<img src="https://static001.geekbang.org/resource/image/7b/81/7b296e168a4900d3b5cb8e57cc3f6181.png" alt="">
### 2. 详细信息
如果你选中详细列表中的一项,右边就会出现该项的详细信息,如下所示:
<img src="https://static001.geekbang.org/resource/image/f7/e6/f76ee3b6b2e6e9629efdd01e6ded57e6.png" alt="">
你可以在此查看请求列表中任意一项的请求行和请求头信息,还可以查看响应行、响应头和响应体。然后你可以根据这些查看的信息来判断你的业务逻辑是否正确,或者有时候也可以用来逆向推导别人网站的业务逻辑。
### 3. 单个资源的时间线
了解了每个资源的详细请求信息之后我们再来分析单个资源请求时间线这就涉及具体的HTTP请求流程了。
<img src="https://static001.geekbang.org/resource/image/1f/e0/1f4f8c194b02975f6d2848b7b73175e0.png" alt="">
我们再回顾下在[《03 | HTTP请求流程为什么很多站点第二次打开速度会很快](https://time.geekbang.org/column/article/116588)这篇文章我们介绍过发起一个HTTP请求之后浏览器首先查找缓存如果缓存没有命中那么继续发起DNS请求获取IP地址然后利用IP地址和服务器端建立TCP连接再发送HTTP请求等待服务器响应不过如果服务器响应头中包含了重定向的信息那么整个流程就需要重新再走一遍。这就是在浏览器中一个HTTP请求的基础流程。
那详细列表中是如何表示出这个流程的呢?这就要重点看下时间线面板了:
<img src="https://static001.geekbang.org/resource/image/ba/af/ba91f06503bda4b4dc4a54901bd7a8af.png" alt="">
那面板中这各项到底是什么含义呢?
**第一个是Queuing**,也就是排队的意思,当浏览器发起一个请求的时候,会有很多原因导致该请求不能被立即执行,而是需要排队等待。导致请求处于排队状态的原因有很多。
- 首先页面中的资源是有优先级的比如CSS、HTML、JavaScript等都是页面中的核心文件所以优先级最高而图片、视频、音频这类资源就不是核心资源优先级就比较低。通常当后者遇到前者时就需要“让路”进入待排队状态。
- 其次我们前面也提到过浏览器会为每个域名最多维护6个TCP连接如果发起一个HTTP请求时这6个TCP连接都处于忙碌状态那么这个请求就会处于排队状态。
- 最后网络进程在为数据分配磁盘空间时新的HTTP请求也需要短暂地等待磁盘分配结束。
等待排队完成之后,就要进入发起连接的状态了。不过在发起连接之前,还有一些原因可能导致连接过程被推迟,这个推迟就表现在面板中的**Stalled**上,它表示停滞的意思。
这里需要额外说明的是,如果你使用了代理服务器,还会增加一个**Proxy Negotiation**阶段,也就是代理协商阶段,它表示代理服务器连接协商所用的时间,不过在上图中没有体现出来,因为这里我们没有使用代理服务器。
接下来,就到了**Initial connection/SSL阶段**了也就是和服务器建立连接的阶段这包括了建立TCP连接所花费的时间不过如果你使用了HTTPS协议那么还需要一个额外的SSL握手时间这个过程主要是用来协商一些加密信息的。关于SSL协商的详细过程我们会在Web安全模块中介绍。
和服务器建立好连接之后,网络进程会准备请求数据,并将其发送给网络,这就是**Request sent阶段**。通常这个阶段非常快因为只需要把浏览器缓冲区的数据发送出去就结束了并不需要判断服务器是否接收到了所以这个时间通常不到1毫秒。
数据发送出去了接下来就是等待接收服务器第一个字节的数据这个阶段称为Waiting (TTFB),通常也称为“**第一字节时间**”。 TTFB是反映服务端响应速度的重要指标对服务器来说TTFB 时间越短,就说明服务器响应越快。
接收到第一个字节之后,进入陆续接收完整数据的阶段,也就是**Content Download阶段**,这意味着从第一字节时间到接收到全部响应数据所用的时间。
## 优化时间线上耗时项
了解了时间线面板上的各项含义之后,我们就可以根据这个请求的时间线来实现相关的优化操作了。
### 1. 排队Queuing时间过久
排队时间过久大概率是由浏览器为每个域名最多维护6个连接导致的。那么基于这个原因你就可以让1个站点下面的资源放在多个域名下面比如放到3个域名下面这样就可以同时支持18个连接了这种方案称为**域名分片**技术。除了域名分片技术外,我个人还建议你**把站点升级到HTTP2**因为HTTP2已经没有每个域名最多维护6个TCP连接的限制了。
### 2. 第一字节时间TTFB时间过久
这可能的原因有如下:
- **服务器生成页面数据的时间过久**。对于动态网页来说,服务器收到用户打开一个页面的请求时,首先要从数据库中读取该页面需要的数据,然后把这些数据传入到模板中,模板渲染后,再返回给用户。服务器在处理这个数据的过程中,可能某个环节会出问题。
- **网络的原因**。比如使用了低带宽的服务器,或者本来用的是电信的服务器,可联通的网络用户要来访问你的服务器,这样也会拖慢网速。
- **发送请求头时带上了多余的用户信息**。比如一些不必要的Cookie信息服务器接收到这些Cookie信息之后可能需要对每一项都做处理这样就加大了服务器的处理时长。
对于这三种问题你要有针对性地出一些解决方案。面对第一种服务器的问题你可以想办法去提高服务器的处理速度比如通过增加各种缓存的技术针对第二种网络问题你可以使用CDN来缓存一些静态文件至于第三种你在发送请求时就去尽可能地减少一些不必要的Cookie数据信息。
### 3. Content Download时间过久
如果单个请求的Content Download花费了大量时间有可能是字节数太多的原因导致的。这时候你就需要减少文件大小比如压缩、去掉源码中不必要的注释等方法。
## 总结
好了,今天就介绍到这里了,下面我来总结下今天的内容。
首先我们简单介绍了Chrome开发者工具10个基础的面板信息然后重点剖析了网络面板再结合之前介绍的网络请求流程来重点分析了网络面板中时间线的各个指标的含义最后我们还简要分析了时间线中各项指标出现异常的可能原因并给出了一些优化方案。
其实通过今天的分析,我们可以得出这样一个结论:如果你要去做一些实践性的项目优化,理解其背后的理论至关重要。因为理论就是一条“线”,它会把各种实践的内容“串”在一起,然后你可以围绕着这条“线”来排查问题。
## 思考时间
今天我们介绍了网络面板还有一个非常重要的Performance面板我们没有介绍不过你可以去网上查找一些相关的资料。
所以今天留给你的是一道实际操作的题目你可以结合网络面板和Performance面板来分析一个Web应用的性能瓶颈比如[https://www.12306.cn](https://www.12306.cn/index/) )。
欢迎在留言区与我分享你的想法,也欢迎你在留言区记录你的思考过程。感谢阅读,如果你觉得这篇文章对你有帮助的话,也欢迎把它分享给更多的朋友。

View File

@@ -0,0 +1,218 @@
<audio id="audio" title="22 | DOM树JavaScript是如何影响DOM树构建的" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/ff/d5/ff2e8f210ed4ade39f5b72902e3c40d5.mp3"></audio>
在[上一篇文章](https://time.geekbang.org/column/article/138844)中,我们通过开发者工具中的网络面板,介绍了网络请求过程的几种**性能指标**以及对页面加载的影响。
而在渲染流水线中后面的步骤都直接或者间接地依赖于DOM结构所以本文我们就继续沿着网络数据流路径来**介绍DOM树是怎么生成的**。然后再基于DOM树的解析流程介绍两块内容第一个是在解析过程中遇到JavaScript脚本DOM解析器是如何处理的第二个是DOM解析器是如何处理跨站点资源的
## 什么是DOM
从网络传给渲染引擎的HTML文件字节流是无法直接被渲染引擎理解的所以要将其转化为渲染引擎能够理解的内部结构这个结构就是DOM。DOM提供了对HTML文档结构化的表述。在渲染引擎中DOM有三个层面的作用。
- 从页面的视角来看DOM是生成页面的基础数据结构。
- 从JavaScript脚本视角来看DOM提供给JavaScript脚本操作的接口通过这套接口JavaScript可以对DOM结构进行访问从而改变文档的结构、样式和内容。
- 从安全视角来看DOM是一道安全防护线一些不安全的内容在DOM解析阶段就被拒之门外了。
简言之DOM是表述HTML的内部数据结构它会将Web页面和JavaScript脚本连接起来并过滤一些不安全的内容。
## DOM树如何生成
在渲染引擎内部,有一个叫**HTML解析器HTMLParser**的模块它的职责就是负责将HTML字节流转换为DOM结构。所以这里我们需要先要搞清楚HTML解析器是怎么工作的。
在开始介绍HTML解析器之前我要先解释一个大家在留言区问到过好多次的问题**HTML解析器是等整个HTML文档加载完成之后开始解析的还是随着HTML文档边加载边解析的**
在这里我统一解答下HTML解析器并不是等整个文档加载完成之后再解析的而是**网络进程加载了多少数据HTML解析器便解析多少数据**。
那详细的流程是怎样的呢网络进程接收到响应头之后会根据响应头中的content-type字段来判断文件的类型比如content-type的值是“text/html”那么浏览器就会判断这是一个HTML类型的文件然后为该请求选择或者创建一个渲染进程。渲染进程准备好之后**网络进程和渲染进程之间会建立一个共享数据的管道**网络进程接收到数据后就往这个管道里面放而渲染进程则从管道的另外一端不断地读取数据并同时将读取的数据“喂”给HTML解析器。你可以把这个管道想象成一个“水管”网络进程接收到的字节流像水一样倒进这个“水管”而“水管”的另外一端是渲染进程的HTML解析器它会动态接收字节流并将其解析为DOM。
解答完这个问题之后接下来我们就可以来详细聊聊DOM的具体生成流程了。
前面我们说过代码从网络传输过来是字节流的形式那么后续字节流是如何转换为DOM的呢你可以参考下图
<img src="https://static001.geekbang.org/resource/image/1b/8c/1bfcd419acf6402c20ffc1a5b1909d8c.png" alt="">
从图中你可以看出字节流转换为DOM需要三个阶段。
**第一个阶段通过分词器将字节流转换为Token。**
前面[《14 | 编译器和解释器V8是如何执行一段JavaScript代码的](https://time.geekbang.org/column/article/131887)文章中我们介绍过V8编译JavaScript过程中的第一步是做词法分析将JavaScript先分解为一个个Token。解析HTML也是一样的需要通过分词器先将字节流转换为一个个Token分为Tag Token和文本Token。上述HTML代码通过词法分析生成的Token如下所示
<img src="https://static001.geekbang.org/resource/image/b1/ac/b16d2fbb77e12e376ac0d7edec20ceac.png" alt="">
由图可以看出Tag Token又分StartTag 和 EndTag比如`&lt;body&gt;`就是StartTag `&lt;/body&gt;就是EndTag`分别对于图中的蓝色和红色块文本Token对应的绿色块。
**至于后续的第二个和第三个阶段是同步进行的需要将Token解析为DOM节点并将DOM节点添加到DOM树中。**
HTML解析器维护了一个**Token栈结构**该Token栈主要用来计算节点之间的父子关系在第一个阶段中生成的Token会被按照顺序压到这个栈中。具体的处理规则如下所示
- 如果压入到栈中的是**StartTag Token**HTML解析器会为该Token创建一个DOM节点然后将该节点加入到DOM树中它的父节点就是栈中相邻的那个元素生成的节点。
- 如果分词器解析出来是**文本Token**那么会生成一个文本节点然后将该节点加入到DOM树中文本Token是不需要压入到栈中它的父节点就是当前栈顶Token所对应的DOM节点。
- 如果分词器解析出来的是**EndTag标签**比如是EndTag divHTML解析器会查看Token栈顶的元素是否是StarTag div如果是就将StartTag div从栈中弹出表示该div元素解析完成。
通过分词器产生的新Token就这样不停地压栈和出栈整个解析过程就这样一直持续下去直到分词器将所有字节流分词完成。
为了更加直观地理解整个过程下面我们结合一段HTML代码如下来一步步分析DOM树的生成过程。
```
&lt;html&gt;
&lt;body&gt;
&lt;div&gt;1&lt;/div&gt;
&lt;div&gt;test&lt;/div&gt;
&lt;/body&gt;
&lt;/html&gt;
```
这段代码以字节流的形式传给了HTML解析器经过分词器处理解析出来的第一个Token是StartTag html解析出来的Token会被压入到栈中并同时创建一个html的DOM节点将其加入到DOM树中。
这里需要补充说明下,**HTML解析器开始工作时会默认创建了一个根为document的空DOM结构**同时会将一个StartTag document的Token压入栈底。然后经过分词器解析出来的第一个StartTag html Token会被压入到栈中并创建一个html的DOM节点添加到document上如下图所示
<img src="https://static001.geekbang.org/resource/image/7a/f1/7a6cd022bd51a3f274cd994b1398bef1.png" alt="">
然后按照同样的流程解析出来StartTag body和StartTag div其Token栈和DOM的状态如下图所示
<img src="https://static001.geekbang.org/resource/image/8c/a5/8c7ba966cebb0050b81c0385ffb4f2a5.png" alt="">
接下来解析出来的是第一个div的文本Token渲染引擎会为该Token创建一个文本节点并将该Token添加到DOM中它的父节点就是当前Token栈顶元素对应的节点如下图所示
<img src="https://static001.geekbang.org/resource/image/dc/af/dc0ddd4e3bf3569555f4b1ebec7a8caf.png" alt="">
再接下来分词器解析出来第一个EndTag div这时候HTML解析器会去判断当前栈顶的元素是否是StartTag div如果是则从栈顶弹出StartTag div如下图所示
<img src="https://static001.geekbang.org/resource/image/c4/a6/c4a255a8881ef9d21e419aa010ce24a6.png" alt="">
按照同样的规则,一路解析,最终结果如下图所示:
<img src="https://static001.geekbang.org/resource/image/aa/2e/aabf14cde38b058c5203195db82ec22e.png" alt="">
通过上面的介绍相信你已经清楚DOM是怎么生成的了。不过在实际生产环境中HTML源文件中既包含CSS和JavaScript又包含图片、音频、视频等文件所以处理过程远比上面这个示范Demo复杂。不过理解了这个简单的Demo生成过程我们就可以往下分析更加复杂的场景了。
## JavaScript是如何影响DOM生成的
我们再来看看稍微复杂点的HTML文件如下所示
```
&lt;html&gt;
&lt;body&gt;
&lt;div&gt;1&lt;/div&gt;
&lt;script&gt;
let div1 = document.getElementsByTagName('div')[0]
div1.innerText = 'time.geekbang'
&lt;/script&gt;
&lt;div&gt;test&lt;/div&gt;
&lt;/body&gt;
&lt;/html&gt;
```
我在两段div中间插入了一段JavaScript脚本这段脚本的解析过程就有点不一样了。`&lt;script&gt;`标签之前,所有的解析流程还是和之前介绍的一样,但是解析到`&lt;script&gt;`标签时渲染引擎判断这是一段脚本此时HTML解析器就会暂停DOM的解析因为接下来的JavaScript可能要修改当前已经生成的DOM结构。
通过前面DOM生成流程分析我们已经知道当解析到script脚本标签时其DOM树结构如下所示
<img src="https://static001.geekbang.org/resource/image/41/54/4150e27b332fab9f5a10bfafb524ff54.png" alt="">
这时候HTML解析器暂停工作JavaScript引擎介入并执行script标签中的这段脚本因为这段JavaScript脚本修改了DOM中第一个div中的内容所以执行这段脚本之后div节点内容已经修改为time.geekbang了。脚本执行完成之后HTML解析器恢复解析过程继续解析后续的内容直至生成最终的DOM。
以上过程应该还是比较好理解的不过除了在页面中直接内嵌JavaScript脚本之外我们还通常需要在页面中引入JavaScript文件这个解析过程就稍微复杂了些如下面代码
```
//foo.js
let div1 = document.getElementsByTagName('div')[0]
div1.innerText = 'time.geekbang'
```
```
&lt;html&gt;
&lt;body&gt;
&lt;div&gt;1&lt;/div&gt;
&lt;script type=&quot;text/javascript&quot; src='foo.js'&gt;&lt;/script&gt;
&lt;div&gt;test&lt;/div&gt;
&lt;/body&gt;
&lt;/html&gt;
```
这段代码的功能还是和前面那段代码是一样的不过这里我把内嵌JavaScript脚本修改成了通过JavaScript文件加载。其整个执行流程还是一样的执行到JavaScript标签时暂停整个DOM的解析执行JavaScript代码不过这里执行JavaScript时需要先下载这段JavaScript代码。这里需要重点关注下载环境因为**JavaScript文件的下载过程会阻塞DOM解析**而通常下载又是非常耗时的会受到网络环境、JavaScript文件大小等因素的影响。
不过Chrome浏览器做了很多优化其中一个主要的优化是**预解析操作**。当渲染引擎收到字节流之后会开启一个预解析线程用来分析HTML文件中包含的JavaScript、CSS等相关文件解析到相关文件之后预解析线程会提前下载这些文件。
再回到DOM解析上我们知道引入JavaScript线程会阻塞DOM不过也有一些相关的策略来规避比如使用CDN来加速JavaScript文件的加载压缩JavaScript文件的体积。另外如果JavaScript文件中没有操作DOM相关代码就可以将该JavaScript脚本设置为异步加载通过async 或defer来标记代码使用方式如下所示
```
&lt;script async type=&quot;text/javascript&quot; src='foo.js'&gt;&lt;/script&gt;
```
```
&lt;script defer type=&quot;text/javascript&quot; src='foo.js'&gt;&lt;/script&gt;
```
async和defer虽然都是异步的不过还有一些差异使用async标志的脚本文件一旦加载完成会立即执行而使用了defer标记的脚本文件需要在DOMContentLoaded事件之前执行。
现在我们知道了JavaScript是如何阻塞DOM解析的了那接下来我们再来结合文中代码看看另外一种情况
```
//theme.css
div {color:blue}
```
```
&lt;html&gt;
&lt;head&gt;
&lt;style src='theme.css'&gt;&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;div&gt;1&lt;/div&gt;
&lt;script&gt;
let div1 = document.getElementsByTagName('div')[0]
div1.innerText = 'time.geekbang' //需要DOM
div1.style.color = 'red' //需要CSSOM
&lt;/script&gt;
&lt;div&gt;test&lt;/div&gt;
&lt;/body&gt;
&lt;/html&gt;
```
该示例中JavaScript代码出现了 `div1.style.color = red'` 的语句它是用来操纵CSSOM的所以在执行JavaScript之前需要先解析JavaScript语句之上所有的CSS样式。所以如果代码里引用了外部的CSS文件那么在执行JavaScript之前还需要等待外部的CSS文件下载完成并解析生成CSSOM对象之后才能执行JavaScript脚本。
而JavaScript引擎在解析JavaScript之前是不知道JavaScript是否操纵了CSSOM的所以渲染引擎在遇到JavaScript脚本时不管该脚本是否操纵了CSSOM都会执行CSS文件下载解析操作再执行JavaScript脚本。
所以说JavaScript脚本是依赖样式表的这又多了一个阻塞过程。至于如何优化我们在下篇文章中再来深入探讨。
通过上面的分析我们知道了JavaScript会阻塞DOM生成而样式文件又会阻塞JavaScript的执行所以在实际的工程中需要重点关注JavaScript文件和样式表文件使用不当会影响到页面性能的。
## 总结
好了,今天就讲到这里,下面我来总结下今天的内容。
首先我们介绍了DOM是如何生成的然后又基于DOM的生成过程分析了JavaScript是如何影响到DOM生成的。因为CSS和JavaScript都会影响到DOM的生成所以我们又介绍了一些加速生成DOM的方案理解了这些能让你更加深刻地理解如何去优化首次页面渲染。
额外说明一下渲染引擎还有一个安全检查模块叫XSSAuditor是用来检测词法安全的。在分词器解析出来Token之后它会检测这些模块是否安全比如是否引用了外部脚本是否符合CSP规范是否存在跨站点请求等。如果出现不符合规范的内容XSSAuditor会对该脚本或者下载任务进行拦截。详细内容我们会在后面的安全模块介绍这里就不赘述了。
## 思考时间
看下面这样一段代码你认为打开这个HTML页面页面显示的内容是什么
```
&lt;html&gt;
&lt;body&gt;
&lt;div&gt;1&lt;/div&gt;
&lt;script&gt;
let div1 = document.getElementsByTagName('div')[0]
div1.innerText = 'time.geekbang'
let div2 = document.getElementsByTagName('div')[1]
div2.innerText = 'time.geekbang.com'
&lt;/script&gt;
&lt;div&gt;test&lt;/div&gt;
&lt;/body&gt;
&lt;/html&gt;
```
欢迎在留言区与我分享你的想法,也欢迎你在留言区记录你的思考过程。感谢阅读,如果你觉得这篇文章对你有帮助的话,也欢迎把它分享给更多的朋友。

View File

@@ -0,0 +1,173 @@
<audio id="audio" title="23 | 渲染流水线CSS如何影响首次加载时的白屏时间" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/3a/ad/3a5fd6a113c9b21441ac4193ed237fad.mp3"></audio>
在[上一篇文章](https://time.geekbang.org/column/article/140140)中我们详细介绍了DOM的生成过程并结合具体例子分析了JavaScript是如何阻塞DOM生成的。那本文我们就继续深入聊聊渲染流水线中的CSS。因为CSS是页面中非常重要的资源它决定了页面最终显示出来的效果并影响着用户对整个网站的第一体验。所以搞清楚浏览器中的CSS是怎么工作的很有必要只有理解了CSS是如何工作的你才能更加深刻地理解如何去优化页面。
本文我们先站在渲染流水线的视角来介绍CSS是如何工作的然后通过CSS的工作流程来分析性能瓶颈最后再来讨论如何减少首次加载时的白屏时间。
## 渲染流水线视角下的CSS
我们先结合下面代码来看看最简单的渲染流程:
```
//theme.css
div{
color : coral;
background-color:black
}
```
```
&lt;html&gt;
&lt;head&gt;
&lt;link href=&quot;theme.css&quot; rel=&quot;stylesheet&quot;&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;div&gt;geekbang com&lt;/div&gt;
&lt;/body&gt;
&lt;/html&gt;
```
这两段代码分别由CSS文件和HTML文件构成我们来分析下打开这段HTML文件时的渲染流水线你可以先参考下面这张渲染流水线示意图
<img src="https://static001.geekbang.org/resource/image/70/18/70a7ea0212ff35fc2be79f1d574ed518.png" alt="">
下面我们结合上图来分析这个页面文件的渲染流水线。
首先是发起主页面的请求这个发起请求方可能是渲染进程也有可能是浏览器进程发起的请求被送到网络进程中去执行。网络进程接收到返回的HTML数据之后将其发送给渲染进程渲染进程会解析HTML数据并构建DOM。这里你需要特别注意下请求HTML数据和构建DOM中间有一段空闲时间这个空闲时间有可能成为页面渲染的瓶颈。
[上一篇文章](https://time.geekbang.org/column/article/140140)中我们提到过当渲染进程接收HTML文件字节流时会先开启一个**预解析线程**如果遇到JavaScript文件或者CSS文件那么预解析线程会提前下载这些数据。对于上面的代码预解析线程会解析出来一个外部的theme.css文件并发起theme.css的下载。这里也有一个空闲时间需要你注意一下就是在DOM构建结束之后、theme.css文件还未下载完成的这段时间内渲染流水线无事可做因为下一步是合成布局树而合成布局树需要CSSOM和DOM所以这里需要等待CSS加载结束并解析成CSSOM。
**那渲染流水线为什么需要CSSOM呢**
和HTML一样渲染引擎也是无法直接理解CSS文件内容的所以需要将其解析成渲染引擎能够理解的结构这个结构就是CSSOM。和DOM一样CSSOM也具有两个作用**第一个是提供给JavaScript操作样式表的能力第二个是为布局树的合成提供基础的样式信息**。这个CSSOM体现在DOM中就是`document.styleSheets`。具体结构你可以去查阅相关资料这里我就不过多介绍了你知道CSSOM的两个作用是怎样的就行了。
有了DOM和CSSOM接下来就可以合成布局树了我们在前面[《05 | 渲染流程HTML、CSS和JavaScript文件是如何变成页面的](https://time.geekbang.org/column/article/118205)这篇文章中讲解过布局树的构造过程这里咱们再简单回顾下。等DOM和CSSOM都构建好之后渲染引擎就会构造布局树。布局树的结构基本上就是复制DOM树的结构不同之处在于DOM树中那些不需要显示的元素会被过滤掉如display:none属性的元素、head标签、script标签等。复制好基本的布局树结构之后渲染引擎会为对应的DOM元素选择对应的样式信息这个过程就是**样式计算**。样式计算完成之后,渲染引擎还需要计算布局树中每个元素对应的几何位置,这个过程就是**计算布局**。通过样式计算和计算布局就完成了最终布局树的构建。再之后,就该进行后续的绘制操作了。
这就是在渲染过程中涉及到CSS的一些主要流程。
了解了这些之后我们再来看看稍微复杂一点的场景还是看下面这段HTML代码
```
//theme.css
div{
color : coral;
background-color:black
}
```
```
&lt;html&gt;
&lt;head&gt;
&lt;link href=&quot;theme.css&quot; rel=&quot;stylesheet&quot;&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;div&gt;geekbang com&lt;/div&gt;
&lt;script&gt;
console.log('time.geekbang.org')
&lt;/script&gt;
&lt;div&gt;geekbang com&lt;/div&gt;
&lt;/body&gt;
&lt;/html&gt;
```
这段代码是我在开头代码的基础之上做了一点小修改在body标签内部加了一个简单的JavaScript。有了JavaScript渲染流水线就有点不一样了可以参考下面这张渲染流水线图
<img src="https://static001.geekbang.org/resource/image/f8/1c/f85f8778f273710ca559a52027ed731c.png" alt="">
那我们就结合这张图来分析含有外部CSS文件和JavaScript代码的页面渲染流水线[上一篇文章](https://time.geekbang.org/column/article/140140)中我们提到过在解析DOM的过程中如果遇到了JavaScript脚本那么需要先暂停DOM解析去执行JavaScript因为JavaScript有可能会修改当前状态下的DOM。
不过在执行JavaScript脚本之前如果页面中包含了外部CSS文件的引用或者通过style标签内置了CSS内容那么渲染引擎还需要将这些内容转换为CSSOM因为JavaScript有修改CSSOM的能力所以在执行JavaScript之前还需要依赖CSSOM。也就是说CSS在部分情况下也会阻塞DOM的生成。
我们再来看看更加复杂一点的情况如果在body中被包含的是JavaScript外部引用文件Demo代码如下所示
```
//theme.css
div{
color : coral;
background-color:black
}
```
```
//foo.js
console.log('time.geekbang.org')
```
```
&lt;html&gt;
&lt;head&gt;
&lt;link href=&quot;theme.css&quot; rel=&quot;stylesheet&quot;&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;div&gt;geekbang com&lt;/div&gt;
&lt;script src='foo.js'&gt;&lt;/script&gt;
&lt;div&gt;geekbang com&lt;/div&gt;
&lt;/body&gt;
&lt;/html&gt;
```
从上面代码可以看出来HTML文件中包含了CSS的外部引用和JavaScript外部文件那它们的渲染流水线是怎样的呢可参考下图
<img src="https://static001.geekbang.org/resource/image/76/1f/7641c75a80133e747aa2faae8f4c8d1f.png" alt="">
从图中可以看出来在接收到HTML数据之后的预解析过程中HTML预解析器识别出来了有CSS文件和JavaScript文件需要下载然后就同时发起这两个文件的下载请求需要注意的是这两个文件的下载过程是重叠的所以下载时间按照最久的那个文件来算。
后面的流水线就和前面是一样的了不管CSS文件和JavaScript文件谁先到达都要先等到CSS文件下载完成并生成CSSOM然后再执行JavaScript脚本最后再继续构建DOM构建布局树绘制页面。
## 影响页面展示的因素以及优化策略
前面我们为什么要花这么多文字来分析渲染流水线呢?主要原因就是**渲染流水线影响到了首次页面展示的速度,而首次页面展示的速度又直接影响到了用户体验**,所以我们分析渲染流水线的目的就是为了找出一些影响到首屏展示的因素,然后再基于这些因素做一些针对性的调整。
那么接下来我们就来看看从发起URL请求开始到首次显示页面的内容在视觉上经历的三个阶段。
- 第一个阶段,等请求发出去之后,到提交数据阶段,这时页面展示出来的还是之前页面的内容。关于提交数据你可以参考前面[《04 | 导航流程从输入URL到页面展示这中间发生了什么](https://time.geekbang.org/column/article/117637)这篇文章。
- 第二个阶段,提交数据之后渲染进程会创建一个空白页面,我们通常把这段时间称为**解析白屏**并等待CSS文件和JavaScript文件的加载完成生成CSSOM和DOM然后合成布局树最后还要经过一系列的步骤准备首次渲染。
- 第三个阶段,等首次渲染完成之后,就开始进入完整页面的生成阶段了,然后页面会一点点被绘制出来。
影响第一个阶段的因素主要是网络或者是服务器处理这块儿,前面文章中我们已经讲过了,这里我们就不再继续分析了。至于第三个阶段,我们会在后续文章中分析,所以这里也不做介绍了。
现在我们重点关注第二个阶段这个阶段的主要问题是白屏时间如果白屏时间过久就会影响到用户体验。为了缩短白屏时间我们来挨个分析这个阶段的主要任务包括了解析HTML、下载CSS、下载JavaScript、生成CSSOM、执行JavaScript、生成布局树、绘制页面一系列操作。
通常情况下的瓶颈主要体现在**下载CSS文件、下载JavaScript文件和执行JavaScript**。
所以要想缩短白屏时长,可以有以下策略:
- 通过内联JavaScript、内联CSS来移除这两种类型的文件下载这样获取到HTML文件之后就可以直接开始渲染流程了。
- 但并不是所有的场合都适合内联那么还可以尽量减少文件大小比如通过webpack等工具移除一些不必要的注释并压缩JavaScript文件。
- 还可以将一些不需要在解析HTML阶段使用的JavaScript标记上async或者defer。
- 对于大的CSS文件可以通过媒体查询属性将其拆分为多个不同用途的CSS文件这样只有在特定的场景下才会加载特定的CSS文件。
通过以上策略就能缩短白屏展示的时长了,不过在实际项目中,总是存在各种各样的情况,这些策略并不能随心所欲地去引用,所以还需要结合实际情况来调整最佳方案。
## 总结
好了,今天就介绍到这里,下面我来总结下今天的内容。
我们首先介绍了CSS在渲染流水线中的位置以及CSS是如何影响到渲染流程的接下来我们通过渲染流水线分析了从发出请求到页面首次绘制的三个阶段最后重点介绍了第二个白屏阶段以及优化该阶段的一些策略。
通过今天的内容我们可以知道虽然JavaScript和CSS给我们带来了极大的便利不过也对页面的渲染带来了很多的限制所以我们要关注资源加载速度需要小心翼翼地处理各种资源之间的关联关系。
## 思考时间
今天留给你的思考题是:当你横屏方向拿着一个手机时,打开一个页面,观察下面几种资源的加载方式,你认为哪几种会阻塞页面渲染?为什么?
```
1:&lt;script src=&quot;foo.js&quot; type=&quot;text/javascript&quot;&gt;&lt;/script&gt;
2:&lt;script defer src=&quot;foo.js&quot; type=&quot;text/javascript&quot;&gt;&lt;/script&gt;
3:&lt;script sync src=&quot;foo.js&quot; type=&quot;text/javascript&quot;&gt;&lt;/script&gt;
4:&lt;link rel=&quot;stylesheet&quot; type=&quot;text/css&quot; href=&quot;foo.css&quot; /&gt;
5:&lt;link rel=&quot;stylesheet&quot; type=&quot;text/css&quot; href=&quot;foo.css&quot; media=&quot;screen&quot;/&gt;
6:&lt;link rel=&quot;stylesheet&quot; type=&quot;text/css&quot; href=&quot;foo.css&quot; media=&quot;print&quot; /&gt;
7:&lt;link rel=&quot;stylesheet&quot; type=&quot;text/css&quot; href=&quot;foo.css&quot; media=&quot;orientation:landscape&quot; /&gt;
8:&lt;link rel=&quot;stylesheet&quot; type=&quot;text/css&quot; href=&quot;foo.css&quot; media=&quot;orientation:portrait&quot; /&gt;
```
欢迎在留言区与我分享你的想法,也欢迎你在留言区记录你的思考过程。感谢阅读,如果你觉得这篇文章对你有帮助的话,也欢迎把它分享给更多的朋友。

View File

@@ -0,0 +1,249 @@
<audio id="audio" title="24 | 分层和合成机制为什么CSS动画比JavaScript高效" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/48/53/482b6d80cf7013da0b18414bfb117c53.mp3"></audio>
在[上一篇文章](https://time.geekbang.org/column/article/140703)中我们分析了CSS和JavaScript是如何影响到DOM树生成的今天我们继续沿着渲染流水线向下分析来聊聊DOM树之后所发生的事情。
在前面[《05 | 渲染流程HTML、CSS和JavaScript文件是如何变成页面的](https://time.geekbang.org/column/article/118205)文章中我们介绍过DOM树生成之后还要经历布局、分层、绘制、合成、显示等阶段后才能显示出漂亮的页面。
本文我们主要讲解渲染引擎的分层和合成机制因为分层和合成机制代表了浏览器最为先进的合成技术Chrome团队为了做到这一点做了大量的优化工作。了解其工作原理有助于拓宽你的视野而且也有助于你更加深刻地理解CSS动画和JavaScript底层工作机制。
## 显示器是怎么显示图像的
每个显示器都有固定的刷新频率通常是60HZ也就是每秒更新60张图片更新的图片都来自于显卡中一个叫**前缓冲区**的地方显示器所做的任务很简单就是每秒固定读取60次前缓冲区中的图像并将读取的图像显示到显示器上。
**那么这里显卡做什么呢?**
显卡的职责就是合成新的图像,并将图像保存到**后缓冲区**中,一旦显卡把合成的图像写到后缓冲区,系统就会让后缓冲区和前缓冲区互换,这样就能保证显示器能读取到最新显卡合成的图像。通常情况下,显卡的更新频率和显示器的刷新频率是一致的。但有时候,在一些复杂的场景中,显卡处理一张图片的速度会变慢,这样就会造成视觉上的卡顿。
## 帧 VS 帧率
了解了显示器是怎么显示图像的之后,下面我们再来明确下帧和帧率的概念,因为这是后续一切分析的基础。
当你通过滚动条滚动页面,或者通过手势缩放页面时,屏幕上就会产生动画的效果。之所以你能感觉到有动画的效果,是因为在滚动或者缩放操作时,渲染引擎会通过渲染流水线生成新的图片,并发送到显卡的后缓冲区。
大多数设备屏幕的更新频率是60次/秒这也就意味着正常情况下要实现流畅的动画效果渲染引擎需要每秒更新60张图片到显卡的后缓冲区。
我们把渲染流水线生成的每一副图片称为一帧把渲染流水线每秒更新了多少帧称为帧率比如滚动过程中1秒更新了60帧那么帧率就是60Hz或者60FPS
由于用户很容易观察到那些丢失的帧,如果在一次动画过程中,渲染引擎生成某些帧的时间过久,那么用户就会感受到卡顿,这会给用户造成非常不好的印象。
要解决卡顿问题就要解决每帧生成时间过久的问题为此Chrome对浏览器渲染方式做了大量的工作其中最卓有成效的策略就是引入了分层和合成机制。分层和合成机制代表了当今最先进的渲染技术所以接下来我们就来分析下什么是合成和渲染技术。
## 如何生成一帧图像
不过在开始之前,我们还需要聊一聊渲染引擎是如何生成一帧图像的。这需要回顾下我们前面[《06 | 渲染流程HTML、CSS和JavaScript文件是如何变成页面的](https://time.geekbang.org/column/article/118826)介绍的渲染流水线。关于其中任意一帧的生成方式,有**重排、重绘**和**合成**三种方式。
这三种方式的渲染路径是不同的,**通常渲染路径越长,生成图像花费的时间就越多**。比如**重排**它需要重新根据CSSOM和DOM来计算布局树这样生成一幅图片时会让整个渲染流水线的每个阶段都执行一遍如果布局复杂的话就很难保证渲染的效率了。而**重绘**因为没有了重新布局的阶段,操作效率稍微高点,但是依然需要重新计算绘制信息,并触发绘制操作之后的一系列操作。
相较于重排和重绘,**合成**操作的路径就显得非常短了并不需要触发布局和绘制两个阶段如果采用了GPU那么合成的效率会非常高。
所以,关于渲染引擎生成一帧图像的几种方式,按照效率我们推荐合成方式优先,若实在不能满足需求,那么就再退后一步使用重绘或者重排的方式。
本文我们的焦点在合成上所以接下来我们就来深入分析下Chrome浏览器是怎么实现合成操作的。Chrome中的合成技术可以用三个词来概括总结**分层、分块**和**合成**。
## 分层和合成
通常页面的组成是非常复杂的有的页面里要实现一些复杂的动画效果比如点击菜单时弹出菜单的动画特效滚动鼠标滚轮时页面滚动的动画效果当然还有一些炫酷的3D动画特效。如果没有采用分层机制从布局树直接生成目标图片的话那么每次页面有很小的变化时都会触发重排或者重绘机制这种“牵一发而动全身”的绘制策略会严重影响页面的渲染效率。
**为了提升每帧的渲染效率Chrome引入了分层和合成的机制。那该怎么来理解分层和合成机制呢**
你可以把一张网页想象成是由很多个图片叠加在一起的每个图片就对应一个图层Chrome合成器最终将这些图层合成了用于显示页面的图片。如果你熟悉PhotoShop的话就能很好地理解这个过程了PhotoShop中一个项目是由很多图层构成的每个图层都可以是一张单独图片可以设置透明度、边框阴影可以旋转或者设置图层的上下位置将这些图层叠加在一起后就能呈现出最终的图片了。
在这个过程中,将素材分解为多个图层的操作就称为**分层**,最后将这些图层合并到一起的操作就称为**合成**。所以,分层和合成通常是一起使用的。
考虑到一个页面被划分为两个层当进行到下一帧的渲染时上面的一帧可能需要实现某些变换如平移、旋转、缩放、阴影或者Alpha渐变这时候合成器只需要将两个层进行相应的变化操作就可以了显卡处理这些操作驾轻就熟所以这个合成过程时间非常短。
**理解了为什么要引入合成和分层机制下面我们再来看看Chrome是怎么实现分层和合成机制的。**
在Chrome的渲染流水线中**分层体现在生成布局树之后**渲染引擎会根据布局树的特点将其转换为层树Layer Tree层树是渲染流水线后续流程的基础结构。
层树中的每个节点都对应着一个图层,下一步的绘制阶段就依赖于层树中的节点。在[《06 | 渲染流程HTML、CSS和JavaScript文件是如何变成页面的](https://time.geekbang.org/column/article/118826)中我们介绍过,绘制阶段其实并不是真正地绘出图片,而是将绘制指令组合成一个列表,比如一个图层要设置的背景为黑色,并且还要在中间画一个圆形,那么绘制过程会生成`|Paint BackGroundColor:Black | Paint Circle|`这样的绘制指令列表,绘制过程就完成了。
有了绘制列表之后,就需要进入光栅化阶段了,光栅化就是按照绘制列表中的指令生成图片。每一个图层都对应一张图片,合成线程有了这些图片之后,会将这些图片合成为“一张”图片,并最终将生成的图片发送到后缓冲区。这就是一个大致的分层、合成流程。
**需要重点关注的是,合成操作是在合成线程上完成的,这也就意味着在执行合成操作时,是不会影响到主线程执行的**。这就是为什么经常主线程卡住了但是CSS动画依然能执行的原因。
## 分块
如果说分层是从宏观上提升了渲染效率,那么分块则是从微观层面提升了渲染效率。
通常情况下,页面的内容都要比屏幕大得多,显示一个页面时,如果等待所有的图层都生成完毕,再进行合成的话,会产生一些不必要的开销,也会让合成图片的时间变得更久。
因此,合成线程会将每个图层分割为大小固定的图块,然后优先绘制靠近视口的图块,这样就可以大大加速页面的显示速度。不过有时候, 即使只绘制那些优先级最高的图块,也要耗费不少的时间,因为涉及到一个很关键的因素——**纹理上传**这是因为从计算机内存上传到GPU内存的操作会比较慢。
为了解决这个问题Chrome又采取了一个策略**在首次合成图块的时候使用一个低分辨率的图片**。比如可以是正常分辨率的一半,分辨率减少一半,纹理就减少了四分之三。在首次显示页面内容的时候,将这个低分辨率的图片显示出来,然后合成器继续绘制正常比例的网页内容,当正常比例的网页内容绘制完成后,再替换掉当前显示的低分辨率内容。这种方式尽管会让用户在开始时看到的是低分辨率的内容,但是也比用户在开始时什么都看不到要好。
## 如何利用分层技术优化代码
通过上面的介绍,相信你已经理解了渲染引擎是怎么将布局树转换为漂亮图片的,理解其中原理之后,你就可以利用分层和合成技术来优化代码了。
在写Web应用的时候你可能经常需要对某个元素做几何形状变换、透明度变换或者一些缩放操作如果使用JavaScript来写这些效果会牵涉到整个渲染流水线所以JavaScript的绘制效率会非常低下。
这时你可以使用 will-change来告诉渲染引擎你会对该元素做一些特效变换CSS代码如下
```
.box {
will-change: transform, opacity;
}
```
这段代码就是提前告诉渲染引擎box元素将要做几何变换和透明度变换操作这时候渲染引擎会将该元素单独实现一帧等这些变换发生时渲染引擎会通过合成线程直接去处理变换这些变换并没有涉及到主线程这样就大大提升了渲染的效率。**这也是CSS动画比JavaScript动画高效的原因**。
所以如果涉及到一些可以使用合成线程来处理CSS特效或者动画的情况就尽量使用will-change来提前告诉渲染引擎让它为该元素准备独立的层。但是凡事都有两面性每当渲染引擎为一个元素准备一个独立层的时候它占用的内存也会大大增加因为从层树开始后续每个阶段都会多一个层结构这些都需要额外的内存所以你需要恰当地使用 will-change。
## 总结
好了,今天就介绍到这里,下面我来总结下今天的内容。
- 首先我们介绍了显示器显示图像的原理,以及帧和帧率的概念,然后基于帧和帧率我们又介绍渲染引擎是如何实现一帧图像的。通常渲染引擎生成一帧图像有三种方式:重排、重绘和合成。其中重排和重绘操作都是在渲染进程的主线程上执行的,比较耗时;而合成操作是在渲染进程的合成线程上执行的,执行速度快,且不占用主线程。
- 然后我们重点介绍了浏览器是怎么实现合成的,其技术细节主要可以使用三个词来概括:分层、分块和合成。
- 最后我们还讲解了CSS动画比JavaScript动画高效的原因以及怎么使用 will-change来优化动画或特效。
## 思考时间
观察下面代码结合Performance面板、内存面板和分层面板全面比较在box中使用 will-change和不使用 will-change的效率、性能和内存占用等情况。
```
&lt;html&gt;
&lt;head&gt;
&lt;title&gt;观察will-change&lt;/title&gt;
&lt;style&gt;
.box {
will-change: transform, opacity;
display: block;
float: left;
width: 40px;
height: 40px;
margin: 15px;
padding: 10px;
border: 1px solid rgb(136, 136, 136);
background: rgb(187, 177, 37);
border-radius: 30px;
transition: border-radius 1s ease-out;
}
body {
font-family: Arial;
}
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;div id=&quot;controls&quot;&gt;
&lt;button id=&quot;start&quot;&gt;start&lt;/button&gt;
&lt;button id=&quot;stop&quot;&gt;stop&lt;/button&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div class=&quot;box&quot;&gt;旋转盒子&lt;/div&gt;
&lt;div class=&quot;box&quot;&gt;旋转盒子&lt;/div&gt;
&lt;div class=&quot;box&quot;&gt;旋转盒子&lt;/div&gt;
&lt;div class=&quot;box&quot;&gt;旋转盒子&lt;/div&gt;
&lt;div class=&quot;box&quot;&gt;旋转盒子&lt;/div&gt;
&lt;div class=&quot;box&quot;&gt;旋转盒子&lt;/div&gt;
&lt;div class=&quot;box&quot;&gt;旋转盒子&lt;/div&gt;
&lt;div class=&quot;box&quot;&gt;旋转盒子&lt;/div&gt;
&lt;div class=&quot;box&quot;&gt;旋转盒子&lt;/div&gt;
&lt;div class=&quot;box&quot;&gt;旋转盒子&lt;/div&gt;
&lt;div class=&quot;box&quot;&gt;旋转盒子&lt;/div&gt;
&lt;div class=&quot;box&quot;&gt;旋转盒子&lt;/div&gt;
&lt;div class=&quot;box&quot;&gt;旋转盒子&lt;/div&gt;
&lt;div class=&quot;box&quot;&gt;旋转盒子&lt;/div&gt;
&lt;div class=&quot;box&quot;&gt;旋转盒子&lt;/div&gt;
&lt;div class=&quot;box&quot;&gt;旋转盒子&lt;/div&gt;
&lt;div class=&quot;box&quot;&gt;旋转盒子&lt;/div&gt;
&lt;div class=&quot;box&quot;&gt;旋转盒子&lt;/div&gt;
&lt;div class=&quot;box&quot;&gt;旋转盒子&lt;/div&gt;
&lt;div class=&quot;box&quot;&gt;旋转盒子&lt;/div&gt;
&lt;div class=&quot;box&quot;&gt;旋转盒子&lt;/div&gt;
&lt;div class=&quot;box&quot;&gt;旋转盒子&lt;/div&gt;
&lt;div class=&quot;box&quot;&gt;旋转盒子&lt;/div&gt;
&lt;div class=&quot;box&quot;&gt;旋转盒子&lt;/div&gt;
&lt;div class=&quot;box&quot;&gt;旋转盒子&lt;/div&gt;
&lt;div class=&quot;box&quot;&gt;旋转盒子&lt;/div&gt;
&lt;div class=&quot;box&quot;&gt;旋转盒子&lt;/div&gt;
&lt;div class=&quot;box&quot;&gt;旋转盒子&lt;/div&gt;
&lt;div class=&quot;box&quot;&gt;旋转盒子&lt;/div&gt;
&lt;div class=&quot;box&quot;&gt;旋转盒子&lt;/div&gt;
&lt;div class=&quot;box&quot;&gt;旋转盒子&lt;/div&gt;
&lt;div class=&quot;box&quot;&gt;旋转盒子&lt;/div&gt;
&lt;div class=&quot;box&quot;&gt;旋转盒子&lt;/div&gt;
&lt;div class=&quot;box&quot;&gt;旋转盒子&lt;/div&gt;
&lt;div class=&quot;box&quot;&gt;旋转盒子&lt;/div&gt;
&lt;div class=&quot;box&quot;&gt;旋转盒子&lt;/div&gt;
&lt;div class=&quot;box&quot;&gt;旋转盒子&lt;/div&gt;
&lt;div class=&quot;box&quot;&gt;旋转盒子&lt;/div&gt;
&lt;div class=&quot;box&quot;&gt;旋转盒子&lt;/div&gt;
&lt;div class=&quot;box&quot;&gt;旋转盒子&lt;/div&gt;
&lt;div class=&quot;box&quot;&gt;旋转盒子&lt;/div&gt;
&lt;div class=&quot;box&quot;&gt;旋转盒子&lt;/div&gt;
&lt;div class=&quot;box&quot;&gt;旋转盒子&lt;/div&gt;
&lt;div class=&quot;box&quot;&gt;旋转盒子&lt;/div&gt;
&lt;div class=&quot;box&quot;&gt;旋转盒子&lt;/div&gt;
&lt;div class=&quot;box&quot;&gt;旋转盒子&lt;/div&gt;
&lt;div class=&quot;box&quot;&gt;旋转盒子&lt;/div&gt;
&lt;div class=&quot;box&quot;&gt;旋转盒子&lt;/div&gt;
&lt;div class=&quot;box&quot;&gt;旋转盒子&lt;/div&gt;
&lt;div class=&quot;box&quot;&gt;旋转盒子&lt;/div&gt;
&lt;div class=&quot;box&quot;&gt;旋转盒子&lt;/div&gt;
&lt;div class=&quot;box&quot;&gt;旋转盒子&lt;/div&gt;
&lt;div class=&quot;box&quot;&gt;旋转盒子&lt;/div&gt;
&lt;div class=&quot;box&quot;&gt;旋转盒子&lt;/div&gt;
&lt;div class=&quot;box&quot;&gt;旋转盒子&lt;/div&gt;
&lt;div class=&quot;box&quot;&gt;旋转盒子&lt;/div&gt;
&lt;div class=&quot;box&quot;&gt;旋转盒子&lt;/div&gt;
&lt;div class=&quot;box&quot;&gt;旋转盒子&lt;/div&gt;
&lt;div class=&quot;box&quot;&gt;旋转盒子&lt;/div&gt;
&lt;div class=&quot;box&quot;&gt;旋转盒子&lt;/div&gt;
&lt;/div&gt;
&lt;script&gt;
let boxes = document.querySelectorAll('.box');
let boxes1 = document.querySelectorAll('.box1');
let start = document.getElementById('start');
let stop = document.getElementById('stop');
let stop_flag = false
start.addEventListener('click', function () {
stop_flag = false
requestAnimationFrame(render);
})
stop.addEventListener('click', function () {
stop_flag = true
})
let rotate_ = 0
let opacity_ = 0
function render() {
if (stop_flag)
return 0
rotate_ = rotate_ + 6
if (opacity_ &gt; 1)
opacity_ = 0
opacity_ = opacity_ + 0.01
let command = 'rotate(' + rotate_ + 'deg)';
for (let index = 0; index &lt; boxes.length; index++) {
boxes[index].style.transform = command
boxes[index].style.opacity = opacity_
}
requestAnimationFrame(render);
}
&lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;
```
欢迎在留言区与我分享你的想法,也欢迎你在留言区记录你的思考过程。感谢阅读,如果你觉得这篇文章对你有帮助的话,也欢迎把它分享给更多的朋友。

View File

@@ -0,0 +1,201 @@
<audio id="audio" title="25 | 页面性能:如何系统地优化页面?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/e9/49/e9651f8db4e8bd2c3a57d5e6a9b3d449.mp3"></audio>
在前面几篇文章中我们分析了页面加载和DOM生成讨论了JavaScript和CSS是如何影响到DOM生成的还结合渲染流水线来讲解了分层和合成机制同时在这些文章里面我们还穿插说明了很多优化页面性能的最佳实践策略。通过这些知识点的学习相信你已经知道渲染引擎是怎么绘制出帧的不过之前我们介绍的内容比较零碎、比较散那么今天我们就来将这些内容系统性地串起来。
那么怎么才能把这些知识点串起来呢?我的思路是从如何系统优化页面速度的角度来切入。
**这里我们所谈论的页面优化,其实就是要让页面更快地显示和响应**。由于一个页面在它不同的阶段,所侧重的关注点是不一样的,所以如果我们要讨论页面优化,就要分析一个页面生存周期的不同阶段。
通常一个页面有三个阶段:**加载阶段、交互阶段和关闭阶段**。
- 加载阶段是指从发出请求到渲染出完整页面的过程影响到这个阶段的主要因素有网络和JavaScript脚本。
- 交互阶段主要是从页面加载完成到用户交互的整合过程影响到这个阶段的主要因素是JavaScript脚本。
- 关闭阶段,主要是用户发出关闭指令后页面所做的一些清理操作。
这里我们需要**重点关注加载阶段和交互阶段**,因为影响到我们体验的因素主要都在这两个阶段,下面我们就来逐个详细分析下。
## 加载阶段
我们先来分析如何系统优化加载阶段中的页面,还是先看一个典型的渲染流水线,如下图所示:
<img src="https://static001.geekbang.org/resource/image/5d/7b/5d8716586b5f4d719097dca881007a7b.jpg" alt="">
观察上面这个渲染流水线,你能分析出来有哪些因素影响了页面加载速度吗?下面我们就先来分析下这个问题。
通过前面文章的讲解你应该已经知道了并非所有的资源都会阻塞页面的首次绘制比如图片、音频、视频等文件就不会阻塞页面的首次渲染而JavaScript、首次请求的HTML资源文件、CSS文件是会阻塞首次渲染的因为在构建DOM的过程中需要HTML和JavaScript文件在构造渲染树的过程中需要用到CSS文件。
我们把**这些能阻塞网页首次渲染的资源称为关键资源**。基于关键资源,我们可以继续细化出来三个影响页面首次渲染的核心因素。
**第一个是关键资源个数**。关键资源个数越多首次页面的加载时间就会越长。比如上图中的关键资源个数就是3个1个HTML文件、1个JavaScript和1个CSS文件。
**第二个是关键资源大小**。通常情况下所有关键资源的内容越小其整个资源的下载时间也就越短那么阻塞渲染的时间也就越短。上图中关键资源的大小分别是6KB、8KB和9KB那么整个关键资源大小就是23KB。
**第三个是请求关键资源需要多少个RTTRound Trip Time**。那什么是RTT呢 在[《02 | TCP协议如何保证页面文件能被完整送达浏览器](https://time.geekbang.org/column/article/113550)这篇文章中我们分析过当使用TCP协议传输一个文件时比如这个文件大小是0.1M由于TCP的特性这个数据并不是一次传输到服务端的而是需要拆分成一个个数据包来回多次进行传输的。**RTT就是这里的往返时延。它是网络中一个重要的性能指标表示从发送端发送数据开始到发送端收到来自接收端的确认总共经历的时延**。通常1个HTTP的数据包在14KB左右所以1个0.1M的页面就需要拆分成8个包来传输了也就是说需要8个RTT。
我们可以结合上图来看看它的关键资源请求需要多少个RTT。首先是请求HTML资源大小是6KB小于14KB所以1个RTT就可以解决了。至于JavaScript和CSS文件这里需要注意一点由于渲染引擎有一个预解析的线程在接收到HTML数据之后预解析线程会快速扫描HTML数据中的关键资源一旦扫描到了会立马发起请求你可以认为JavaScript和CSS是同时发起请求的所以它们的请求是重叠的那么计算它们的RTT时只需要计算体积最大的那个数据就可以了。这里最大的是CSS文件9KB所以我们就按照9KB来计算同样由于9KB小于14KB所以JavaScript和CSS资源也就可以算成1个RTT。也就是说上图中关键资源请求共花费了2个RTT。
了解了影响加载过程中的几个核心因素之后,接下来我们就可以系统性地考虑优化方案了。**总的优化原则就是减少关键资源个数降低关键资源大小降低关键资源的RTT次数**。
- 如何减少关键资源的个数一种方式是可以将JavaScript和CSS改成内联的形式比如上图的JavaScript和CSS若都改成内联模式那么关键资源的个数就由3个减少到了1个。另一种方式如果JavaScript代码没有DOM或者CSSOM的操作则可以改成async或者defer属性同样对于CSS如果不是在构建页面之前加载的则可以添加媒体取消阻止显现的标志。当JavaScript标签加上了async或者defer、CSSlink 属性之前加上了取消阻止显现的标志后,它们就变成了非关键资源了。
- 如何减少关键资源的大小可以压缩CSS和JavaScript资源移除HTML、CSS、JavaScript文件中一些注释内容也可以通过前面讲的取消CSS或者JavaScript中关键资源的方式。
- 如何减少关键资源RTT的次数可以通过减少关键资源的个数和减少关键资源的大小搭配来实现。除此之外还可以使用CDN来减少每次RTT时长。
在优化实际的页面加载速度时,你可以先画出优化之前关键资源的图表,然后按照上面优化关键资源的原则去优化,优化完成之后再画出优化之后的关键资源图表。
## 交互阶段
接下来我们再来聊聊页面加载完成之后的交互阶段以及应该如何去优化。谈交互阶段的优化,其实就是在谈渲染进程渲染帧的速度,因为在交互阶段,帧的渲染速度决定了交互的流畅度。因此讨论页面优化实际上就是讨论渲染引擎是如何渲染帧的,否则就无法优化帧率。
我们先来看看交互阶段的渲染流水线如下图。和加载阶段的渲染流水线有一些不同的地方是在交互阶段没有了加载关键资源和构建DOM、CSSOM流程通常是由JavaScript触发交互动画的。
<img src="https://static001.geekbang.org/resource/image/4a/0c/4a942e53f9358c9c4634c310335cc10c.png" alt="">
结合上图我们来一起回顾下交互阶段是如何生成一个帧的。大部分情况下生成一个新的帧都是由JavaScript通过修改DOM或者CSSOM来触发的。还有另外一部分帧是由CSS来触发的。
如果在计算样式阶段发现有布局信息的修改,那么就会触发**重排**操作,然后触发后续渲染流水线的一系列操作,这个代价是非常大的。
同样如果在计算样式阶段没有发现有布局信息的修改,只是修改了颜色一类的信息,那么就不会涉及到布局相关的调整,所以可以跳过布局阶段,直接进入绘制阶段,这个过程叫**重绘**。不过重绘阶段的代价也是不小的。
还有另外一种情况通过CSS实现一些变形、渐变、动画等特效这是由CSS触发的并且是在合成线程上执行的这个过程称为合成。因为它不会触发重排或者重绘而且合成操作本身的速度就非常快所以执行合成是效率最高的方式。
回顾了在交互过程中的帧是如何生成的,那接下来我们就可以讨论优化方案了。**一个大的原则就是让单个帧的生成速度变快**。所以,下面我们就来分析下在交互阶段渲染流水线中有哪些因素影响了帧的生成速度以及如何去优化。
### 1. 减少JavaScript脚本执行时间
有时JavaScript函数的一次执行时间可能有几百毫秒这就严重霸占了主线程执行其他渲染任务的时间。针对这种情况我们可以采用以下两种策略
- 一种是将一次执行的函数分解为多个任务,使得每次的执行时间不要过久。
- 另一种是采用Web Workers。你可以把Web Workers当作主线程之外的一个线程在Web Workers中是可以执行JavaScript脚本的不过Web Workers中没有DOM、CSSOM环境这意味着在Web Workers中是无法通过JavaScript来访问DOM的所以我们可以把一些和DOM操作无关且耗时的任务放到Web Workers中去执行。
总之在交互阶段对JavaScript脚本总的原则就是不要一次霸占太久主线程。
### 2. 避免强制同步布局
在介绍强制同步布局之前我们先来聊聊正常情况下的布局操作。通过DOM接口执行添加元素或者删除元素等操作后是需要重新计算样式和布局的不过正常情况下这些操作都是在另外的任务中异步完成的这样做是为了避免当前的任务占用太长的主线程时间。为了直观理解你可以参考下面的代码
```
&lt;html&gt;
&lt;body&gt;
&lt;div id=&quot;mian_div&quot;&gt;
&lt;li id=&quot;time_li&quot;&gt;time&lt;/li&gt;
&lt;li&gt;geekbang&lt;/li&gt;
&lt;/div&gt;
&lt;p id=&quot;demo&quot;&gt;强制布局demo&lt;/p&gt;
&lt;button onclick=&quot;foo()&quot;&gt;添加新元素&lt;/button&gt;
&lt;script&gt;
function foo() {
let main_div = document.getElementById(&quot;mian_div&quot;)
let new_node = document.createElement(&quot;li&quot;)
let textnode = document.createTextNode(&quot;time.geekbang&quot;)
new_node.appendChild(textnode);
document.getElementById(&quot;mian_div&quot;).appendChild(new_node);
}
&lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;
```
对于上面这段代码我们可以使用Performance工具来记录添加元素的过程如下图所示
<img src="https://static001.geekbang.org/resource/image/32/c9/32b6a645646f99fc3517fb0b5e003cc9.png" alt="">
从图中可以看出来执行JavaScript添加元素是在一个任务中执行的重新计算样式布局是在另外一个任务中执行这就是正常情况下的布局操作。
理解了正常情况下的布局操作,接下来我们就可以聊什么是强制同步布局了。
**所谓强制同步布局是指JavaScript强制将计算样式和布局操作提前到当前的任务中**。为了直观理解,这里我们对上面的代码做了一点修改,让它变成强制同步布局,修改后的代码如下所示:
```
function foo() {
let main_div = document.getElementById(&quot;mian_div&quot;)
let new_node = document.createElement(&quot;li&quot;)
let textnode = document.createTextNode(&quot;time.geekbang&quot;)
new_node.appendChild(textnode);
document.getElementById(&quot;mian_div&quot;).appendChild(new_node);
//由于要获取到offsetHeight
//但是此时的offsetHeight还是老的数据
//所以需要立即执行布局操作
console.log(main_div.offsetHeight)
}
```
将新的元素添加到DOM之后我们又调用了`main_div.offsetHeight`来获取新main_div的高度信息。如果要获取到main_div的高度就需要重新布局所以这里在获取到main_div的高度之前JavaScript还需要强制让渲染引擎默认执行一次布局操作。我们把这个操作称为强制同步布局。
同样你可以看下面通过Performance记录的任务状态
<img src="https://static001.geekbang.org/resource/image/ce/d9/ce951be7a38e2ef1a9a23a1c7e84b1d9.png" alt="">
从上图可以看出来,计算样式和布局都是在当前脚本执行过程中触发的,这就是强制同步布局。
为了避免强制同步布局我们可以调整策略在修改DOM之前查询相关值。代码如下所示
```
function foo() {
let main_div = document.getElementById(&quot;mian_div&quot;)
//为了避免强制同步布局在修改DOM之前查询相关值
console.log(main_div.offsetHeight)
let new_node = document.createElement(&quot;li&quot;)
let textnode = document.createTextNode(&quot;time.geekbang&quot;)
new_node.appendChild(textnode);
document.getElementById(&quot;mian_div&quot;).appendChild(new_node);
}
```
### 3. 避免布局抖动
还有一种比强制同步布局更坏的情况那就是布局抖动。所谓布局抖动是指在一次JavaScript执行过程中多次执行强制布局和抖动操作。为了直观理解你可以看下面的代码
```
function foo() {
let time_li = document.getElementById(&quot;time_li&quot;)
for (let i = 0; i &lt; 100; i++) {
let main_div = document.getElementById(&quot;mian_div&quot;)
let new_node = document.createElement(&quot;li&quot;)
let textnode = document.createTextNode(&quot;time.geekbang&quot;)
new_node.appendChild(textnode);
new_node.offsetHeight = time_li.offsetHeight;
document.getElementById(&quot;mian_div&quot;).appendChild(new_node);
}
}
```
我们在一个for循环语句里面不断读取属性值每次读取属性值之前都要进行计算样式和布局。执行代码之后使用Performance记录的状态如下所示
<img src="https://static001.geekbang.org/resource/image/36/87/36159f7081e37ce4714b20ce2630e987.png" alt="">
从上图可以看出在foo函数内部重复执行计算样式和布局这会大大影响当前函数的执行效率。这种情况的避免方式和强制同步布局一样都是尽量不要在修改DOM结构时再去查询一些相关值。
### 4. 合理利用CSS合成动画
合成动画是直接在合成线程上执行的这和在主线程上执行的布局、绘制等操作不同如果主线程被JavaScript或者一些布局任务占用CSS动画依然能继续执行。所以要尽量利用好CSS合成动画如果能让CSS处理动画就尽量交给CSS来操作。
另外如果能提前知道对某个元素执行动画操作那就最好将其标记为will-change这是告诉渲染引擎需要将该元素单独生成一个图层。
### 5. 避免频繁的垃圾回收
我们知道JavaScript使用了自动垃圾回收机制如果在一些函数中频繁创建临时对象那么垃圾回收器也会频繁地去执行垃圾回收策略。这样当垃圾回收操作发生时就会占用主线程从而影响到其他任务的执行严重的话还会让用户产生掉帧、不流畅的感觉。
所以要尽量避免产生那些临时垃圾数据。那该怎么做呢?可以尽可能优化储存结构,尽可能避免小颗粒对象的产生。
## 总结
好了,今天就介绍到这里,下面我来总结下本文的主要内容。
我们主要讲解了如何系统优化加载阶段和交互阶段的页面。
在加载阶段核心的优化原则是优化关键资源的加载速度减少关键资源的个数降低关键资源的RTT次数。
在交互阶段核心的优化原则是尽量减少一帧的生成时间。可以通过减少单次JavaScript的执行时间、避免强制同步布局、避免布局抖动、尽量采用CSS的合成动画、避免频繁的垃圾回收等方式来减少一帧生成的时长。
## 思考时间
那你来分析下新浪官网([https://www.sina.com.cn/](https://www.sina.com.cn/) )在加载阶段和交互阶段所存在的一些性能问题。
欢迎在留言区与我分享你的想法,也欢迎你在留言区记录你的思考过程。感谢阅读,如果你觉得这篇文章对你有帮助的话,也欢迎把它分享给更多的朋友。

View File

@@ -0,0 +1,93 @@
<audio id="audio" title="26 | 虚拟DOM虚拟DOM和实际的DOM有何不同" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/31/81/3144fa5b3c3e82a02e830be28a05a481.mp3"></audio>
虚拟DOM是最近非常火的技术两大著名前端框架React和Vue都使用了虚拟DOM所以我觉得非常有必要结合浏览器的工作机制对虚拟DOM进行一次分析。当然了React和Vue框架本身所蕴含的知识点非常多而且也不是我们专栏的重点所以在这里我们还是把重心聚焦在虚拟DOM上。
在本文我们会先聊聊DOM的一些缺陷然后在此基础上介绍虚拟DOM是如何解决这些缺陷的最后再站在双缓存和MVC的视角来聊聊虚拟DOM。理解了这些会让你对目前的前端框架有一个更加底层的认识这也有助于你更好地理解这些前端框架。
## DOM的缺陷
通过前面一系列文章的学习你对DOM的生成过程应该已经有了比较深刻的理解并且也知道了通过JavaScript操纵DOM是会影响到整个渲染流水线的。另外DOM还提供了一组JavaScript接口用来遍历或者修改节点这套接口包含了getElementById、removeChild、appendChild等方法。
比如,我们可以调用`document.body.appendChild(node)`往body节点上添加一个元素调用该API之后会引发一系列的连锁反应。首先渲染引擎会将node节点添加到body节点之上然后触发样式计算、布局、绘制、栅格化、合成等任务我们把这一过程称为**重排**。除了重排之外,还有可能引起**重绘**或者**合成**操作,形象地理解就是“**牵一发而动全身**”。另外对于DOM的不当操作还有可能引发**强制同步布局**和**布局抖动**的问题这些操作都会大大降低渲染效率。因此对于DOM的操作我们时刻都需要非常小心谨慎。
当然对于简单的页面来说其DOM结构还是比较简单的所以以上这些操作DOM的问题并不会对用户体验产生太多影响。但是对于一些复杂的页面或者目前使用非常多的单页应用来说其DOM结构是非常复杂的而且还需要不断地去修改DOM树每次操作DOM渲染引擎都需要进行重排、重绘或者合成等操作因为DOM结构复杂所生成的页面结构也会很复杂对于这些复杂的页面执行一次重排或者重绘操作都是非常耗时的这就给我们带来了真正的性能问题。
所以我们需要有一种方式来减少JavaScript对DOM的操作这时候虚拟DOM就上场了。
## 什么是虚拟DOM
在谈论什么是虚拟DOM之前我们先来看看虚拟DOM到底要解决哪些事情。
- 将页面改变的内容应用到虚拟DOM上而不是直接应用到DOM上。
- 变化被应用到虚拟DOM上时虚拟DOM并不急着去渲染页面而仅仅是调整虚拟DOM的内部状态这样操作虚拟DOM的代价就变得非常轻了。
- 在虚拟DOM收集到足够的改变时再把这些变化一次性应用到真实的DOM上。
基于以上三点我们再来看看什么是虚拟DOM。为了直观理解你可以参考下图
<img src="https://static001.geekbang.org/resource/image/cf/90/cf2089ad62af94881757c2f2de277890.png" alt="">
该图是我结合React流程画的一张虚拟DOM执行流程图下面我们就结合这张图来分析下虚拟DOM到底怎么运行的。
- **创建阶段**。首先依据JSX和基础数据创建出来虚拟DOM它反映了真实的DOM树的结构。然后由虚拟DOM树创建出真实DOM树真实的DOM树生成完后再触发渲染流水线往屏幕输出页面。
- **更新阶段**。如果数据发生了改变那么就需要根据新的数据创建一个新的虚拟DOM树然后React比较两个树找出变化的地方并把变化的地方一次性更新到真实的DOM树上最后渲染引擎更新渲染流水线并生成新的页面。
既然聊到虚拟DOM的更新那我们就不得不聊聊最新的**React Fiber更新机制**。通过上图我们知道当有数据更新时React会生成一个新的虚拟DOM然后拿新的虚拟DOM和之前的虚拟DOM进行比较这个过程会找出变化的节点然后再将变化的节点应用到DOM上。
这里我们重点关注下比较过程最开始的时候比较两个虚拟DOM的过程是在一个递归函数里执行的其**核心算法是reconciliation**。通常情况下这个比较过程执行得很快不过当虚拟DOM比较复杂的时候执行比较函数就有可能占据主线程比较久的时间这样就会导致其他任务的等待造成页面卡顿。为了解决这个问题React团队重写了reconciliation算法新的算法称为Fiber reconciler之前老的算法称为Stack reconciler。
在前面[《20 | async/await使用同步的方式去写异步代码》](https://time.geekbang.org/column/article/137827)那篇文章中我们介绍了协程其实协程的另外一个称呼就是Fiber所以在这里我们可以把Fiber和协程关联起来那么所谓的Fiber reconciler相信你也很清楚了就是在执行算法的过程中出让主线程这样就解决了Stack reconciler函数占用时间过久的问题。至于具体的实现过程在这里我就不详细分析了如果感兴趣的话你可以自行查阅相关资料进行学习。
了解完虚拟DOM的大致执行流程你应该也就知道为何需要虚拟DOM了。不过以上都从单纯的技术视角来分析虚拟DOM的那接下来我们再从双缓存和MVC模型这两个视角来聊聊虚拟DOM。
### 1. 双缓存
在开发游戏或者处理其他图像的过程中,屏幕从前缓冲区读取数据然后显示。但是很多图形操作都很复杂且需要大量的运算,比如一幅完整的画面,可能需要计算多次才能完成,如果每次计算完一部分图像,就将其写入缓冲区,那么就会造成一个后果,那就是在显示一个稍微复杂点的图像的过程中,你看到的页面效果可能是一部分一部分地显示出来,因此在刷新页面的过程中,会让用户感受到界面的闪烁。
而使用双缓存,可以让你先将计算的中间结果存放在另一个缓冲区中,等全部的计算结束,该缓冲区已经存储了完整的图形之后,再将该缓冲区的图形数据一次性复制到显示缓冲区,这样就使得整个图像的输出非常稳定。
在这里你可以把虚拟DOM看成是DOM的一个buffer和图形显示一样它会在完成一次完整的操作之后再把结果应用到DOM上这样就能减少一些不必要的更新同时还能保证DOM的稳定输出。
### 2. MVC模式
到这里我们了解了虚拟DOM是一种类似双缓存的实现。不过如果站在技术角度来理解虚拟缓存依然不能全面理解其含义。那么接下来我们再来看看虚拟DOM在MVC模式中所扮演的角色。
在各大设计模式当中MVC是一个非常重要且应用广泛的模式因为它能将数据和视图进行分离在涉及到一些复杂的项目时能够大大减轻项目的耦合度使得程序易于维护。
关于MVC的基础结构你可以先参考下图
<img src="https://static001.geekbang.org/resource/image/4c/a6/4c03b5882878dcce2df01c1e2e8db8a6.png" alt="">
通过上图你可以发现MVC的整体结构比较简单由模型、视图和控制器组成其**核心思想就是将数据和视图分离**也就是说视图和模型之间是不允许直接通信的它们之间的通信都是通过控制器来完成的。通常情况下的通信路径是视图发生了改变然后通知控制器控制器再根据情况判断是否需要更新模型数据。当然还可以根据不同的通信路径和控制器不同的实现方式基于MVC又能衍生出很多其他的模式如MVP、MVVM等不过万变不离其宗它们的基础骨架都是基于MVC而来。
所以在分析基于React或者Vue这些前端框架时我们需要先重点把握大的MVC骨架结构然后再重点查看通信方式和控制器的具体实现方式这样我们就能从架构的视角来理解这些前端框架了。比如在分析React项目时我们可以把React的部分看成是一个MVC中的视图在项目中结合Redux就可以构建一个MVC的模型结构如下图所示
<img src="https://static001.geekbang.org/resource/image/e0/03/e024ba6c212a1d6bfa01b327e987e103.png" alt="">
在该图中我们可以把虚拟DOM看成是MVC的视图部分其控制器和模型都是由Redux提供的。其具体实现过程如下
- 图中的控制器是用来监控DOM的变化一旦DOM发生变化控制器便会通知模型让其更新数据
- 模型数据更新好之后,控制器会通知视图,告诉它模型的数据发生了变化;
- 视图接收到更新消息之后会根据模型所提供的数据来生成新的虚拟DOM
- 新的虚拟DOM生成好之后就需要与之前的虚拟DOM进行比较找出变化的节点
- 比较出变化的节点之后React将变化的虚拟节点应用到DOM上这样就会触发DOM节点的更新
- DOM节点的变化又会触发后续一系列渲染流水线的变化从而实现页面的更新。
在实际工程项目中,你需要学会分析出这各个模块,并梳理出它们之间的通信关系,这样对于任何框架你都能轻松上手了。
## 总结
好了,今天就介绍到这里,下面我来总结下本文的主要内容。
首先我们分析了直接操作DOM会触发渲染流水线的一系列反应如果对DOM操作不当的话甚至还会触发强制同步布局和布局抖动的问题这也是我们在操作DOM时需要非常小心谨慎的原因。
在此分析的基础上我们介绍了虚拟DOM是怎么解决直接操作DOM所带来的问题以及React Fiber更新机制。
要聊前端框架就绕不开设计模式所以接下来我们又从双缓存和MVC角度分析了虚拟DOM。双缓存是一种经典的思路应用在很多场合能解决页面无效刷新和闪屏的问题虚拟DOM就是双缓存思想的一种体现。而基于MVC的设计思想也广泛地渗透到各种场合并且基于MVC又衍生出了很多其他模式如MVP、MVVM等不过万变不离其宗它们的基础骨架都是基于MVC而来。站在MVC视角来理解虚拟DOM能让你看到更为“广阔的世界”。
## 思考时间
今天留给你的思考题是虚拟DOM都解决了哪些问题
欢迎在留言区与我分享你的想法,也欢迎你在留言区记录你的思考过程。感谢阅读,如果你觉得这篇文章对你有帮助的话,也欢迎把它分享给更多的朋友。

View File

@@ -0,0 +1,92 @@
<audio id="audio" title="27 | 渐进式网页应用PWA它究竟解决了Web应用的哪些问题" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/e3/e7/e32654aa66ba7ada9a13fb47db15e2e7.mp3"></audio>
在专栏[开篇词](https://time.geekbang.org/column/article/113399)中,我们提到过浏览器的三大进化路线:
- 第一个是应用程序Web化
- 第二个是Web应用移动化
- 第三个是Web操作系统化
其中第二个Web应用移动化是Google梦寐以求而又一直在发力的一件事不过对于移动设备来说前有本地App后有移动小程序想要浏览器切入到移动端是相当困难的一件事因为浏览器的运行性能是低于本地App的并且Google也没有类似微信或者Facebook这种体量的用户群体。
但是要让浏览器切入到移动端让其取得和原生应用同等待遇可是Google的梦想那该怎么做呢
这就是我们本节要聊的PWA。那什么是PWAPWA又是以什么方式切入到移动端的呢
PWA全称是Progressive Web App翻译过来就是渐进式网页应用。根据字面意思它就是“渐进式+Web应用”。对于Web应用很好理解了就是目前我们普通的Web页面所以PWA所支持的首先是一个Web页面。至于“渐进式”就需要从下面两个方面来理解。
- 站在Web应用开发者来说PWA提供了一个渐进式的过渡方案让Web应用能逐步具有本地应用的能力。采取渐进式可以降低站点改造的代价使得站点逐步支持各项新技术而不是一步到位。
- 站在技术角度来说PWA技术也是一个渐进式的演化过程在技术层面会一点点演进比如逐渐提供更好的设备特性支持不断优化更加流畅的动画效果不断让页面的加载速度变得更快不断实现本地应用的特性。
从这两点可以看出来PWA采取的是非常一个缓和的渐进式策略不再像以前那样激进动不动就是取代本地App、取代小程序。与之相反而是要充分发挥Web的优势渐进式地缩短和本地应用或者小程序的距离。
那么Web最大的优势是什么呢我认为是自由开放也正是因为自由和开放所以大家就很容易对同一件事情达成共识达成共识之后一套代码就可以运行在各种设备之上了这就是跨平台这也恰恰是本地应用所不具备的。而对于小程序倒是可以实现跨平台但要让各家达成共识目前来看似乎还是非常不切实际的。
所以我给PWA的定义就是**它是一套理念渐进式增强Web的优势并通过技术手段渐进式缩短和本地应用或者小程序的距离**。基于这套理念之下的技术都可以归类到PWA。
那今天我们就主要来聊聊PWA主要采用了哪些技术手段来缩短它和本地应用或者小程序的距离。
## Web应用 VS 本地应用
那相对于本地应用Web页面到底缺少了什么
- 首先Web应用缺少离线使用能力在离线或者在弱网环境下基本上是无法使用的。而用户需要的是沉浸式的体验在离线或者弱网环境下能够流畅地使用是用户对一个应用的基本要求。
- 其次Web应用还缺少了消息推送的能力因为作为一个App厂商需要有将消息送达到应用的能力。
- 最后Web应用缺少一级入口也就是将Web应用安装到桌面在需要的时候直接从桌面打开Web应用而不是每次都需要通过浏览器来打开。
**针对以上Web缺陷PWA提出了两种解决方案通过引入Service Worker来试着解决离线存储和消息推送的问题通过引入manifest.json来解决一级入口的问题**。下面我们就来详细分析下Service Worker是如何工作的。
## 什么是Service Worker
我们先来看看 Service Worker是怎么解决离线存储和消息推送的问题。
其实在Service Worker之前WHATWG小组就推出过用App Cache标准来缓存页面不过在使用过程中App Cache所暴露的问题比较多遭到多方吐槽所以这个标准最终也只能被废弃了可见一个成功的标准是需要经历实践考量的。
所以在2014年的时候标准委员会就提出了Service Worker的概念它的主要思想是**在页面和网络之间增加一个拦截器,用来缓存和拦截请求**。整体结构如下图所示:
<img src="https://static001.geekbang.org/resource/image/23/12/23b97b087c346cdd378b26b2d158e812.png" alt="">
在没有安装Service Worker之前WebApp都是直接通过网络模块来请求资源的。安装了Service Worker模块之后WebApp请求资源时会先通过Service Worker让它判断是返回Service Worker 缓存的资源还是重新去网络请求资源。一切的控制权都交由Service Worker来处理。
## Service Worker的设计思路
现在我们知道Service Worker的主要功能就是拦截请求和缓存资源接下来我们就从Web应用的需求角度来看看Service Worker的设计思路。
### 1. 架构
通过前面**页面循环系统**的分析我们已经知道了JavaScript和页面渲染流水线的任务都是在页面主线程上执行的如果一段JavaScript执行时间过久那么就会阻塞主线程使得渲染一帧的时间变长从而让用户产生卡顿的感觉这对用户来说体验是非常不好的。
为了避免JavaScript过多占用页面主线程时长的情况浏览器实现了Web Worker的功能。Web Worker的目的是让JavaScript能够运行在页面主线程之外不过由于Web Worker中是没有当前页面的DOM环境的所以在Web Worker中只能执行一些和DOM无关的JavaScript脚本并通过postMessage方法将执行的结果返回给主线程。所以说在Chrome中 Web Worker其实就是在渲染进程中开启的一个新线程它的生命周期是和页面关联的。
**“让其运行在主线程之外”就是Service Worker来自Web Worker的一个核心思想**。不过Web Worker是临时的每次JavaScript脚本执行完成之后都会退出执行结果也不能保存下来如果下次还有同样的操作就还得重新来一遍。所以Service Worker需要在Web Worker的基础之上加上储存功能。
另外由于Service Worker还需要会为多个页面提供服务所以还**不能把Service Worker和单个页面绑定起来**。在目前的Chrome架构中Service Worker是运行在浏览器进程中的因为浏览器进程生命周期是最长的所以在浏览器的生命周期内能够为所有的页面提供服务。
### 2. 消息推送
**消息推送也是基于Service Worker来实现的**。因为消息推送时浏览器页面也许并没有启动这时就需要Service Worker来接收服务器推送的消息并将消息通过一定方式展示给用户。关于消息推送的细节这里我们就不详述了如果你感兴趣的话可以自行搜索相关资料去学习。
### 3. 安全
基于Web应用的业务越来越多了其安全问题是不可忽视的所以在设计Service Worker之初安全问题就被提上了日程。
关于安全其中最为核心的一条就是HTTP。我们知道HTTP采用的是明文传输信息存在被窃听、被篡改和被劫持的风险在项目中使用HTTP来传输数据无疑是“裸奔”。所以在设计之初就考虑对Service Worker采用HTTPS协议因为采用HTTPS 的通信数据都是经过加密的即便拦截了数据也无法破解数据内容而且HTTPS还有校验机制通信双方很容易知道数据是否被篡改。关于HTTPS协议我们会在最后的安全模块详细介绍。
所以要使站点支持Service Worker首先必要的一步就是要将站点升级到HTTPS。
除了必须要使用HTTPSService Worker还需要同时支持Web页面默认的安全策略诸如同源策略、内容安全策略CSP关于这些后续我们也会详细介绍。
## 总结
好了,今天就介绍到这里,下面我来总结下本文的主要内容。
我们先分析了PWA它是由很多技术组成的一个理念其核心思想是**渐进式**。对于开发者它提供了非常温和的方式让开发者将普通的站点逐步过渡到Web应用。对于技术本身而言它是渐进式演进逐渐将Web技术发挥到极致的同时也逐渐缩小和本地应用的差距。在此基础上我们又分析了PWA中的Service Worker的设计思路。
另外PWA 还提供了 manifest.json 配置文件可以让开发者自定义桌面的图标、显示名称、启动方式等信息还可以设置启动画面、页面主题颜色等信息。关于manifest.json的配置还是比较简单的详细使用教程网上有很多这里我就不做介绍了。
添加桌面标、增加离线缓存、增加消息推送等功能是PWA走向设备的必备功能但我认为真正决定PWA能否崛起的还是底层技术比如页面渲染效率、对系统设备的支持程度、WebAssembly等而这些技术也在渐进式进化过程中。所以未来如何我们拭目以待。
## 思考时间
预测未来最好的方式是理解现在那么今天我留给你的思考题是你觉得PWA能进入移动设备吗
欢迎在留言区与我分享你的想法,也欢迎你在留言区记录你的思考过程。感谢阅读,如果你觉得这篇文章对你有帮助的话,也欢迎把它分享给更多的朋友。

View File

@@ -0,0 +1,183 @@
<audio id="audio" title="28 | WebComponent像搭积木一样构建Web应用" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/fa/de/faaa698ccc8bdc4bd5c5ae95a688dcde.mp3"></audio>
在[上一篇文章](https://time.geekbang.org/column/article/144983)中我们从技术演变的角度介绍了PWA这是一套集合了多种技术的理念让浏览器渐进式适应设备端。今天我们要站在开发者和项目角度来聊聊WebComponent同样它也是一套技术的组合能提供给开发者组件化开发的能力。
那什么是组件化呢?
其实组件化并没有一个明确的定义不过这里我们可以使用10个字来形容什么是组件化那就是**对内高内聚,对外低耦合**。对内各个元素彼此紧密结合、相互依赖,对外和其他组件的联系最少且接口简单。
可以说,程序员对组件化开发有着天生的需求,因为一个稍微复杂点的项目,就涉及到多人协作开发的问题,每个人负责的组件需要尽可能独立完成自己的功能,其组件的内部状态不能影响到别人的组件,在需要和其他组件交互的地方得提前协商好接口。通过组件化可以降低整个系统的耦合度,同时也降低程序员之间沟通复杂度,让系统变得更加易于维护。
使用组件化能带来很多优势所以很多语言天生就对组件化提供了很好的支持比如C/C++就可以很好地将功能封装成模块无论是业务逻辑还是基础功能抑或是UI都能很好地将其组合在一起实现组件内部的高度内聚、组件之间的低耦合。
大部分语言都能实现组件化,归根结底在于编程语言特性,大多数语言都有自己的函数级作用域、块级作用域和类,可以将内部的状态数据隐藏在作用域之下或者对象的内部,这样外部就无法访问了,然后通过约定好的接口和外部进行通信。
JavaScript虽然有不少缺点但是作为一门编程语言它也能很好地实现组件化毕竟有自己的函数级作用域和块级作用域所以封装内部状态数据并提供接口给外部都是没有问题的。
既然JavaScript可以很好地实现组件化那么我们所谈论的WebComponent到底又是什么呢
## 阻碍前端组件化的因素
在前端虽然HTML、CSS和JavaScript是强大的开发语言但是在大型项目中维护起来会比较困难如果在页面中嵌入第三方内容时还需要确保第三方的内容样式不会影响到当前内容同样也要确保当前的DOM不会影响到第三方的内容。
所以要聊WebComponent得先看看HTML和CSS是如何阻碍前端组件化的这里我们就通过下面这样一个简单的例子来分析下
```
&lt;style&gt;
p {
background-color: brown;
color: cornsilk
}
&lt;/style&gt;
&lt;p&gt;time.geekbang.org&lt;/p&gt;
```
```
&lt;style&gt;
p {
background-color: red;
color: blue
}
&lt;/style&gt;
&lt;p&gt;time.geekbang&lt;/p&gt;
```
上面这两段代码分别实现了自己p标签的属性如果两个人分别负责开发这两段代码的话那么在测试阶段可能没有什么问题不过当最终项目整合的时候其中内部的CSS属性会影响到其他外部的p标签的之所以会这样是因为CSS是影响全局的。
我们在[《23 | 渲染流水线CSS如何影响首次加载时的白屏时间](https://time.geekbang.org/column/article/140703)这篇文章中分析过渲染引擎会将所有的CSS内容解析为CSSOM在生成布局树的时候会在CSSOM中为布局树中的元素查找样式所以有两个相同标签最终所显示出来的效果是一样的渲染引擎是不能为它们分别单独设置样式的。
除了CSS的全局属性会阻碍组件化DOM也是阻碍组件化的一个因素因为在页面中只有一个DOM任何地方都可以直接读取和修改DOM。所以使用JavaScript来实现组件化是没有问题的但是JavaScript一旦遇上CSS和DOM那么就相当难办了。
## WebComponent组件化开发
现在我们了解了**CSS和DOM是阻碍组件化的两个因素**,那要怎么解决呢?
WebComponent给出了解决思路它提供了对局部视图封装能力可以让DOM、CSSOM和JavaScript运行在局部环境中这样就使得局部的CSS和DOM不会影响到全局。
了解了这些下面我们就结合具体代码来看看WebComponent是怎么实现组件化的。
前面我们说了WebComponent是一套技术的组合具体涉及到了**Custom elements自定义元素、Shadow DOM影子DOM<strong>和**HTML templatesHTML模板</strong>详细内容你可以参考MDN上的[相关链接](https://developer.mozilla.org/zh-CN/docs/Web/Web_Components)。
下面我们就来演示下这3个技术是怎么实现数据封装的如下面代码所示
```
&lt;!DOCTYPE html&gt;
&lt;html&gt;
&lt;body&gt;
&lt;!--
一:定义模板
定义内部CSS样式
定义JavaScript行为
--&gt;
&lt;template id=&quot;geekbang-t&quot;&gt;
&lt;style&gt;
p {
background-color: brown;
color: cornsilk
}
div {
width: 200px;
background-color: bisque;
border: 3px solid chocolate;
border-radius: 10px;
}
&lt;/style&gt;
&lt;div&gt;
&lt;p&gt;time.geekbang.org&lt;/p&gt;
&lt;p&gt;time1.geekbang.org&lt;/p&gt;
&lt;/div&gt;
&lt;script&gt;
function foo() {
console.log('inner log')
}
&lt;/script&gt;
&lt;/template&gt;
&lt;script&gt;
class GeekBang extends HTMLElement {
constructor() {
super()
//获取组件模板
const content = document.querySelector('#geekbang-t').content
//创建影子DOM节点
const shadowDOM = this.attachShadow({ mode: 'open' })
//将模板添加到影子DOM上
shadowDOM.appendChild(content.cloneNode(true))
}
}
customElements.define('geek-bang', GeekBang)
&lt;/script&gt;
&lt;geek-bang&gt;&lt;/geek-bang&gt;
&lt;div&gt;
&lt;p&gt;time.geekbang.org&lt;/p&gt;
&lt;p&gt;time1.geekbang.org&lt;/p&gt;
&lt;/div&gt;
&lt;geek-bang&gt;&lt;/geek-bang&gt;
&lt;/body&gt;
&lt;/html&gt;
```
详细观察上面这段代码我们可以得出要使用WebComponent通常要实现下面三个步骤。
**首先使用template属性来创建模板**。利用DOM可以查找到模板的内容但是模板元素是不会被渲染到页面上的也就是说DOM树中的template节点不会出现在布局树中所以我们可以使用template来自定义一些基础的元素结构这些基础的元素结构是可以被重复使用的。一般模板定义好之后我们还需要在模板的内部定义样式信息。
**其次我们需要创建一个GeekBang的类**。在该类的构造函数中要完成三件事:
1. 查找模板内容;
1. 创建影子DOM
1. 再将模板添加到影子DOM上。
上面最难理解的是影子DOM其实影子DOM的作用是将模板中的内容与全局DOM和CSS进行隔离这样我们就可以实现元素和样式的私有化了。你可以把影子DOM看成是一个作用域其内部的样式和元素是不会影响到全局的样式和元素的而在全局环境下要访问影子DOM内部的样式或者元素也是需要通过约定好的接口的。
总之通过影子DOM我们就实现了CSS和元素的封装在创建好封装影子DOM的类之后我们就可以**使用customElements.define来自定义元素了**(可参考上述代码定义元素的方式)。
**最后就很简单了可以像正常使用HTML元素一样使用该元素**,如上述代码中的`&lt;geek-bang&gt;&lt;/geek-bang&gt;`
上述代码最终渲染出来的页面,如下图所示:
<img src="https://static001.geekbang.org/resource/image/57/7c/579c65e2d2221f4e476c7846b842c27c.png" alt="">
从图中我们可以看出影子DOM内部的样式是不会影响到全局CSSOM的。另外使用DOM接口也是无法直接查询到影子DOM内部元素的比如你可以使用`document.getElementsByTagName('div')`来查找所有div元素这时候你会发现影子DOM内部的元素都是无法查找的因为要想查找影子DOM内部的元素需要专门的接口所以通过这种方式又将影子内部的DOM和外部的DOM进行了隔离。
通过影子DOM可以隔离CSS和DOM不过需要注意一点影子DOM的JavaScript脚本是不会被隔离的比如在影子DOM定义的JavaScript函数依然可以被外部访问这是因为JavaScript语言本身已经可以很好地实现组件化了。
## 浏览器如何实现影子DOM
关于WebComponent的使用方式我们就介绍到这里。WebComponent整体知识点不多内容也不复杂我认为核心就是影子DOM。上面我们介绍影子DOM的作用主要有以下两点
1. 影子DOM中的元素对于整个网页是不可见的
1. 影子DOM的CSS不会影响到整个网页的CSSOM影子DOM内部的CSS只对内部的元素起作用。
那么浏览器是如何实现影子DOM的呢下面我们就来分析下如下图
<img src="https://static001.geekbang.org/resource/image/5b/22/5bce3d00c8139a7fde9cc90f9d803322.png" alt="">
该图是上面那段示例代码对应的DOM结构图从图中可以看出我们使用了两次geek-bang属性那么就会生成两个影子DOM并且每个影子DOM都有一个shadow root的根节点我们可以将要展示的样式或者元素添加到影子DOM的根节点上每个影子DOM你都可以看成是一个独立的DOM它有自己的样式、自己的属性内部样式不会影响到外部样式外部样式也不会影响到内部样式。
浏览器为了实现影子DOM的特性在代码内部做了大量的条件判断比如当通过DOM接口去查找元素时渲染引擎会去判断geek-bang属性下面的shadow-root元素是否是影子DOM如果是影子DOM那么就直接跳过shadow-root元素的查询操作。所以这样通过DOM API就无法直接查询到影子DOM的内部元素了。
另外当生成布局树的时候渲染引擎也会判断geek-bang属性下面的shadow-root元素是否是影子DOM如果是那么在影子DOM内部元素的节点选择CSS样式的时候会直接使用影子DOM内部的CSS属性。所以这样最终渲染出来的效果就是影子DOM内部定义的样式。
## 总结
好了,今天就讲到这里,下面我来总结下本文的主要内容。
首先我们介绍了组件化开发是程序员的刚需所谓组件化就是功能模块要实现高内聚、低耦合的特性。不过由于DOM和CSSOM都是全局的所以它们是影响了前端组件化的主要元素。基于这个原因就出现WebComponent它包含自定义元素、影子DOM和HTML模板三种技术使得开发者可以隔离CSS和DOM。在此基础上我们还重点介绍了影子DOM到底是怎么实现的。
关于WebComponent的未来如何这里我们不好预测和评判但是有一点可以肯定WebComponent也会采用渐进式迭代的方式向前推进未来依然有很多坑需要去填。
## 思考时间
今天留给你的思考题是你是怎么看待WebComponents和前端框架React、Vue之间的关系的
欢迎在留言区与我分享你的想法,也欢迎你在留言区记录你的思考过程。感谢阅读,如果你觉得这篇文章对你有帮助的话,也欢迎把它分享给更多的朋友。