Tuland的小院
type: channel
link: http://120.25.166.245/
description: Something's going to happen~
items: 大白话也能讲清楚的 React17 原理（1）—— 搭建可调试源码的项目
items: 如何系统化解释BFC
items: 【5.4k字+25张原创图】不信你看完还不懂HTTPS（图解HTTPS）
items: 【实战】ServiceWorker实现离线缓存
items: 浏览器的本地存储方案
items: 浏览器缓存策略详解
items: 【译】shadow DOM 真的能提高样式性能表现吗？

大白话也能讲清楚的 React17 原理（1）—— 搭建可调试源码的项目
type: item
date: 2024-03-14T12:06:00+08:00
description: ### 前言 如果不想麻烦，可以直接在[我的git地址][1]中克隆项目至本地，使用`npm i`安装依赖包，然后`npm run start`启动即可。大 TODO：大白话讲React原理目录： ### 创建 React17 项目 #### 先使用官方脚手架`create-react-app`创建项目 ``` npx create-react-app learn-react-source-code ``` #### 降级 React 版本 我们需要把 React 版本从 18 降到 17 。 ``` "@testing-library/react": "^12.1.5", "react": "^17.0.2", "react-dom": "^17.0.2", ``` ![构建2.jpg][2] #### 修改src/index.js ``` import React from "react"; import ReactDOM from "react-dom"; import "./index.css"; import App from "./App"; import reportWebVitals from "./reportWebVitals"; const root = document.getElementById("root"); ReactDOM.render( , root ); ``` ![构建3.jpg][3] #### 尝试运行项目 **删除**根目录下`package-lock.json`，重新安装依赖 `npm i` ，最后 `npm run start`。 此时应该能成功运行项目了，但是目前开发者工具`sources`面板里文件还是打包后的react代码。 为了调试源码，我们还需要做以下步骤 ### 创建带源码的项目 #### 在 src 目录下，克隆 React17 源码 ``` cd src git clone https://github.com/facebook/react.git -b 17.0.2 ``` #### 根目录下，释放 webpack 配置 ``` npm run eject or yarn eject ``` 因为此时 `webpack` 配置全部集成在`react-script`中，我们需要将他释放开来，让我们自己配置 `webpack` 。 命令执行完毕后在新增的 `config` 文件夹下可以看到 `webpack` 的配置文件： ![构建8.jpg][4] #### 根目录下，配置 webpack 使其引用源码 往`config/webpack.config.js` 添加 `alias` ``` // ./config/webpack.config.js ... alias: { ... ...(modules.webpackAliases || {}), + react: path.resolve(__dirname, "../src/react/packages/react"), + "react-dom": path.resolve(__dirname, "../src/react/packages/react-dom"), + shared: path.resolve(__dirname, "../src/react/packages/shared"), + "react-reconciler": path.resolve( __dirname, "../src/react/packages/react-reconciler" ), + "legacy-events": path.resolve( __dirname, "../src/react/packages/legacy-events" ), + scheduler: path.resolve(__dirname, "../src/react/packages/scheduler"), }, ``` ![构建5.jpg][5] #### 关闭 EsLint ``` // ./config/webpack.config.js ... const disableESLintPlugin = true; ``` ![构建4.jpg][6] #### `config/env.js`添加环境变量 ``` // ./config/env.js ... // Stringify all values so we can feed into webpack DefinePlugin const stringified = { + __DEV__: true, + __PROFILE__: true, + __UMD__…

如何系统化解释BFC
type: item
date: 2024-03-01T16:20:00+08:00
description: ### 前言 前些天被人问到了 BFC ，在回答的时候俺支支吾吾半天只能说个大概，没办法有条理性的告诉他 BFC 是什么有什么用。查阅资料，发现也没有能够从源头上系统化梳理BFC的文章，遂自己查阅文档写出本文。 ### 什么是BFC？ BFC 全称是`Block Formatting Context`，即**块格式化上下文**。它是 CSS2.1 规范定义的，关于 CSS 渲染定位的一个概念。要明白 BFC 到底是什么，还得首先聊聊 CSS 中的**视觉格式化模型**。 ### 什么是视觉格式化模型、什么是盒子类型 写了这么久Web的我们都知道：块级元素换行、行内元素不换行、浮动和绝对定位脱离文档流等等。 以上都代表了浏览器对某种节点类型的处理方式，这些处理方式就是由**视觉格式化模型**定义的。 **视觉格式化模型**，即 `Visual Formatting Model`，是用来处理文档并将它显示在视觉媒体（浏览器、移动端）上的机制，它也是CSS中的一个概念，或者说，**一种规则**。 那么它是如何工作的呢? > In the visual formatting model, each element in the document tree generates zero or more boxes according to the box model. 首先，**视觉格式化模型**定义了**盒子**（Box）这个概念，————文档树中的每个元素，都属于某种盒子。它可用于文档元素的**定位、布局和格式化**，盒子类型主要包括了： 1. 块盒子 2. 行内盒子 3. 匿名盒子（没有名字不能被选择器选中的盒） 这边的术语非常多，而且不能乱用，[具体可参考MDN文档](https://developer.mozilla.org/zh-CN/docs/Web/CSS/Visual_formatting_model) 。以下只提取最大级的概念。 [CSS的盒子模型](https://developer.mozilla.org/zh-CN/docs/Web/CSS/CSS_box_model/Introduction_to_the_CSS_box_model)就是我们熟知的`content-box`和`border-box`那些，这里就不多展开了。 盒子的类型由元素的 `display`属性决定： ``` display: block;//list-item table display: inline;//inline-block inline-table display: inherit; ``` #### 块盒子 Block Box 1. 当元素的 `display`为 `block`、`list-item` 或 `table` 时，该元素将成为**块级元素**( block-level element )。 2. 每个**块级元素**都会至少生成一个**块级盒子**（ block-level box ）。 3. 每个**块级盒子**都会参与[块格式化上下文](https://developer.mozilla.org/zh-CN/docs/Web/CSS/CSS_display/Block_formatting_context)的创建，即BFC。 4. 还有一种类型：**块容器盒子**（ block container box ）：它要么只包含其他**块级盒子**，要么只包含**行内盒子**并同时创建一个[行内格式化上下文](https://developer.mozilla.org/zh-CN/docs/Web/CSS/Inline_formatting_context)，即IFC。。 5. 块盒子 = 既是**块级盒子**又是**块容器盒子**的盒子。 简单理解：每个**块级元素**至少生成一个**块级盒子**，参与**BFC**，在视觉上表现为：呈现为块，竖直排列。 #### 行内盒子 Inline Box 1. 如果一个元素的 `display`属性为 `inline`、`inline-block` 或 `inline-table`，则称该元素为**行内级元素**( inline-level element )。 2. **行内级元素**生成**行内级盒子** （ inline-level box ）。 3. **行内级盒子**可能会参与[行内格式化上下文（ inline formatting context ）](https://developer.mozilla.org/zh-CN/docs/Web/CSS/Inline_formatting_context)的创建，即IFC。 4. **行内盒子** = 参与了行内…

【5.4k字+25张原创图】不信你看完还不懂HTTPS（图解HTTPS）
type: item
date: 2022-12-26T21:14:00+08:00
description: ### 前言： 本文的行文思路来自最近看的一本书[《图解HTTP》][1]，所有流程图均由自己在[Excalidraw][2]上原创，如需引用，请标明出处，也欢迎在本文评论区留言探讨，如果对您有帮助，请不要吝啬您的点赞和收藏~ ### 背景 都3202年了，互联网如此发达的现在，我们经历过的网站和 Web 应用也越来越多，而细心的你可能会发现，几乎每个我们经常使用的网站都使用 HTTPS 协议传输数据，原因在于 HTTPS 相较于 HTTP 能够提供更加安全的服务。 浏览器对于使用 HTTP 协议的网站都会加上⚠︎标志表示数据传输不安全，而对于使用 HTTPS 协议的网站则会表示安全。 ![差别1.jpg][3] 那么是什么让各个“有头有脸”的大公司都纷纷选用 HTTPS 呢？这就不得不提 HTTP 协议在安全性方面上的三个缺点了： ### HTTP的缺点： 1. 通信使用明文（不加密），**内容可能会被窃听。** 2. 不验证通信方的身份，**通信方有可能遭遇伪装。** 3. 无法证明报文的完整性，**有可能接收到的报文已遭篡改。** ### HTTPS来解决HTTP的安全性问题： 针对上文提到的三个方面的问题，我们的 HTTPS 协议堂堂诞生！那么它是怎么解决这三个问题的呢？ 1. 会被窃听？**数据加密！**： HTTPS 通信不再使用明文，而是使用加密后的**密文**。 2. 会被伪装？**数字证书！**： HTTPS 通信双方通过数字证书机构（CA）颁布的**数字证书**来证明双方身份。 3. 会被篡改？**完整性保护！**： HTTPS 通过**摘要算法**得到传输数据的摘要，如果数据被篡改，接收方算出的摘要将发生改变，接收方就知道传输数据已被篡改。 通过上面的介绍，我们也就可以回答这个经典问题了————**“什么是HTTPS？”** 答：**HTTP + 加密 + 认证 + 完整性保护 = HTTPS** ### HTTPS 是身披 SSL/TLS 外壳的 HTTP 其实 HTTPS 并非是应用层的一种新协议，只是在 HTTP 通信部分接口用 SSL （安全套接层）和 TLS （安全传输层协议）代替了而已： 1. 通常情况下，HTTP 直接和 TCP 通信。 2. 当使用 HTTPS 时，则变成 HTTP 先和 SSL/TLS 通信，再由 SSL 和 TCP 通信。所以也可以说：`HTTPS = SSL/TLS + HTTP` ![差别2.jpg][4] SSL/TLS 是世界上应用最为广泛的网络安全技术，在 HTTP 采用了他们之后，也就拥有了**加密 + 认证 + 完整性保护**这些功能。而且他们还不仅可以用在 HTTP 上，他们也可以用在任何需要加密的应用层协议上，如 SMTP 、Telnet 等 ### 数据加密解决明文传输被窃听问题 > 在对 SSL/TLS 进行讲解之前，我们先来浅浅地了解一下加密算法们，如有错误请一定指出。 > ps:从本章开始，我们都将通信双方假定为客户端和服务器，并且通常由客户端充当发送方，服务器充当接收方。 为了防止传输数据被黑客所窃听，客户端与服务器之间需要对数据进行加解密处理，如图所示，**客户端**使用**加密算法**将**明文**转变为**密文**，而**服务器**接收到**密文**后，使用**解密算法**将密文转变回**明文**，这样即使在通信中途密文被窃听了，黑客也**无法获得有效的信息**。 ![加密算法1.jpg][5] 近代的加密方法中，**加密算法是公开的，而`密钥`却是保密的**。加密和解密都会用到`密钥`，没有`密钥`就无法对密文进行解密，反过来说，任何人只需要持有`密钥`，就能进行解密了。我们一般将加密算法分为两大类： 1. 对称加密：加密和解密时，使用相同的密钥。图中**密钥A = 密钥B**。 2. 非对称加密：加密和解密时，使用不同的密钥。图中**密钥A ≠ 密钥B**。 #### 对称加密 对称加密在加密和解密时，使用的是相同的`密钥`。 凯撒加密是一种最简单且最广为人知的加密算法，它是一种替换加密的技术：明文中的所有字母都在字母表上向后（或向前）按照一个**固定数目**进行偏移后被替换成密文。 ![凯撒加密.jpg][6] 如上图，每个字母**都向后偏移了4个字母**的位置。那对于对称加密来说，这个**偏移量**就是`密钥`。 ![凯撒加密2.jpg][7] 当然，凯撒加密过于简单了，肯定不算是行之有效的加密算法。 为了增加解密的难度，对称加密内部又细分为**流加密**和**分组加密**两种类型，现在常用的对称加密算法有** DES 、3DES 、 AES 、RC4 **等。这里就不过多展开了。 ##### 对称加密的优缺点 - 优点：简单快…

【实战】ServiceWorker实现离线缓存
type: item
date: 2022-05-04T10:52:00+08:00
description: 本文出现的所有代码都可以从[我的git地址][1]中找到，如果对你有帮助，还请点点`star`，欢迎提出您宝贵的意见。 你也可以在此[狠狠地尝试Demo][2]（记得打开谷歌开发者工具查看NetWork） ### 背景 先前已经在我的另一篇文章：[浏览器缓存策略详解][3]中提到了`Service Worker`，其实它是一个特别大的概念，我们今天就来稍微深入地学习一下他的`cacheStorage`缓存功能。相信大家都听说过PWA（Progressive Web APP）——渐进式网页；致力于实现与原生 APP 相似的交互体验。PWA总体具有以下特点：（以下特点来自[知乎][4]） - **渐进式**：适用于选用任何浏览器的所有用户，因为它是以渐进式增强作为核心宗旨来开发的。 - **自适应**：适合**任何机型**：桌面设备、移动设备、平板电脑或任何未来设备。 - **连接无关性**：能够借助于服务工作线程在**离线**或低质量网络状况下工作。 - **离线推送**：使用推送消息通知，能够让我们的应用像`Native App`一样，提升用户体验。 - **及时更新**：在服务工作线程更新进程的作用下时刻保持最新状态。 - **安全性**：通过 HTTPS 提供，以防止窥探和确保内容不被篡改。 总结下来，PWA的实现其实主要依赖于以下三点： 1. `manifest` 实现手机主界面的`web app`图标、添加进桌面、标题、`icon`等； 2. `Service Worker`实现离线缓存请求、更新缓存、删除缓存；（用插件实现文件更新即版本号更新从而缓存更新） 3. 前端`registration`实现用户订阅，后端`web-push`实现消息推送，前端`Service Woker`监听`push`实现消息通知，但是Chrome需要能够连接到外网，因为push用的是谷歌的云服务。 这里就主要谈谈如何使用`Service Worker`实现离线缓存： ### 什么是Service Worker? 众所周知， js 是被设计为单线程语言，因为主要用途是与用户互动和操作`DOM`，单线程设计可以简化并发问题，避免多线程并发时的竞态条件、死锁和其他问题。但单线程存在的问题是，**GUI线程**和**js线程**需要抢占资源，在 js 执行比较耗时的逻辑时，容易造成页面假死，用户体验较差。后来`html5`开放了`Web Worker`可以在浏览器后台挂载新线程。它无法直接操作`DOM`，无法访问`window`、`document`等对象。而`Service Worker`可以说是`Web Worker`进一步发展后的产物。**`SW`也是运行在浏览器背后的独立线程，**主要用于代理网页请求，可缓存请求结果；可实现离线缓存功能；可跨页面通信。也拥有单独的作用域范围和运行环境 ![浏览器进程.jpg][5] ### Service Worker的特点 在`Service Worker`诞生之前，`Web Worker`就已经“服役”很久了，他们都是独立于js线程外的线程，但是`Web Worker`有个特点就是：当网页关闭时，`Web Worker`就失效了，而`Service Worker`的诞生就是为了解决这个问题的，它具有以下特点： 1. 一旦被`install`，就永远存在，除非被手动`unregister`。 2. 拥有自己独立的**worker线程**，独立于当前网页进程，有自己独立的**worker上下文**(context)。 3. 用到的时候就可以直接唤醒，不用的时候自动睡眠。 4. 可拦截代理`fetch`请求和响应，不支持`xmlHttpRequest`请求。 5. 可操作缓存文件，且缓存文件可以被网页进程取到（包括网路离线状态）。 6. 能向客户推送消息。 7. 不能直接操作`DOM`、`window`、`parent`等 。（但是它有自己的**`self`**对象来代替`window`） 8. 必须在`HTTPS`环境下才能工作。(本地调试可以用`localhost`) 9. 异步实现，内部大都是通过`Promise`实现，以防止浏览器卡顿。**所以`Service Worker`的各类操作都被设计为异步**，我们在调用的时候要**使用`Promise`语法**。 ### Service Worker的生命周期 当我们注册了`Service Worker`后，它会经历生命周期的各个阶段，同时会触发相应的事件。整个生命周期包括了：`installing` --> `installed` --> `activating` --> `activated` --> `redundant`。当`Service Wo…

浏览器的本地存储方案
type: item
date: 2022-03-16T20:39:00+08:00
description: # 浏览器知识系列之本地存储 ### 什么是“本地存储”？ 浏览器本地存储是一种机制，允许Web应用程序在浏览器端**存储数据**，以便用户在下次访问时可以快速获取和使用这些数据。 ### 浏览器本地存储的种类 浏览器的本地存储主要分为： 1. **Cookie** 2. **WebStorage** ——分为`localStorage`和`sessionStorage` 3. **IndexedDB** ### Cookie 其实从起源上来说，`Cookie`的诞生是为了解决`HTTP`是**无状态协议**这个问题的，最开始用`HTTP`通信，由于没法记录状态，服务器不能清楚地知道客户端是谁，也就无法帮他省下复杂操作（如多次登陆），`Cookie`就是用来解决这个问题的。 #### Cookie的构成： `Cookie`本质上只是一小段文本字符串，该字符串一般保存在用户计算机特定文件夹下的某个文件中，它的内部格式是键值对。其中又有一些比较重要的属性： 1. **name=value**：键值对，可以设置要保存的Key/Value。 2. **expires和max-age**：`Cookie`的过期时间，如果这个属性不指定的话，就是`session cookie`，也就是这个`session`在关闭浏览器后就会被删除。 1. `expires`是一个`GMT`格式的时间串。 2. `max-age`在`HTTP/1.1`中代替`expires`，是以**秒为单位**的时间段。`Cookie`失效时刻=创建时刻+`max-age` 3. **path**：相当于`Cookie`的作用域，子文件夹下的网页可以访问父文件夹下的网页生成的`Cookie`，但是反过来却不能，例如： 1. 路径`/`：生成`cookie_a` 2. 路径`/sub`生成`cookie_b` 3. 则`/`路径只能读取`cookie_a`，而`/sub`路径可以读取`cookie_a`和`cookie_b`。 注意，如果不设置这个属性的话，默认使用的是当前的`url`的相对路径。一般我们设置`Cookie`，就是让网站中所有其他的网页都能读取到的，所以应该将其设置为`'/'`。 4. **domain**：`Cookie`的所属域名，默认是全域名，也可以自己指定根域名。例如在`www.tuland.com`上设置`Cookie`，`domin`就是`tuland.com`，这样的话如果当前网站旗下有子域名网站的话，例如`a.tuland.com`，`b.tuland.com`之类的子域名，那么在子域名所在网站中也能读取到这个`Cookie`。 5. **secure**：这个属性是针对`HTTPS`来说的，如果设定其为`true`的话，那么只有在`HTTPS`协议下才会携带`Cookie`。默认为空。 6. **httponly**：这个属性设置`js`对于当前`cookie`的读取权限，如果为`true`，则`js`无法**读取、修改、删除**当前`Cookie`。默认为空 ![Cookie1.jpg][1] #### Cookie的使用和设置 可以分为三个步骤： 1. **服务器发送`Cookie`**：当客户端第一次访问服务器时，服务器会通过**响应头**要求客户端设置`Cookie`。例如：`**Set-Cookie**:name=tuland; Path=/` 2. **客户端接收并保存`Cookie`**：客户端接收到`Cookie`后，会将其保存在本地。 3. **客户端再次请求时携带`Cookie`**：当客户端再次请求服务器时，会将之前保存的`Cookie`发送到服务器端，例如 `Cookie:name=tuland;`。此外，客户端JS脚本也可以通过`document.cooke = "name='tuland'"`等方式来设置`Cookie`。 #### Cookie的一些缺点 1. 存储大小只有**4kb**。 2. `HTTP`请求时会携带`Cookie`，会增加请求的数据量。 3. 只能用`document.cooke = ...`方式修改，太过简陋。 4. 不同的浏览器，`Cookie`有不同的上限。（`Chrome`没有） #### Cookie的安全性 虽然`Cookie`在WEB应用中非常有用，但是我们在使用过程中也需要注意一些安全问题： 1. `Cookie`可能被客户端篡改，所以在使用前我们需要验证其合法性。 2. 不要在`Cookie`中存储敏感数据，比如用户密码，账户余额。 3. 使用`httpOnly`属性可以防止通过`JS`脚本读取到`Cookie`信息，从而防止`XSS`攻击。 4. 尽量减少`Cooki…

浏览器缓存策略详解
type: item
date: 2022-03-01T16:47:00+08:00
description: ![缓存头图.jpg][1] # 浏览器知识系列之浏览器缓存 ### 什么是缓存? 浏览器缓存是一种用于提高网站性能的技术，它通过在**本地磁盘，内存或缓存服务器**中存储网站的HTML、CSS、JavaScript、图片等文件副本，以便在用户再次访问相同的URL时，浏览器可以直接从本地或缓存服务器中加载这些资源，而**无需重新从服务器下载**。这样做可以减少网络带宽消耗，降低服务器的负担，并提升用户体验。 ### 说得这么玄乎，我要怎么看到缓存呢？ 查看缓存的办法很简单，以`Chrome`浏览器为例子，打开开发者工具，进入`NetWork`页面，在`Size`这一栏我们就可以看到这样的资源加载情况： ![缓存1.jpg][2] 如果`size`是`memory cache`和`disk cache`就表示：浏览器并没有向服务器发送请求，而是直接读取了本地的缓存资源文件。至于什么是`memery cache`什么是`disk cache`，我们暂且按下不表，之后在介绍缓存位置的时候会介绍到。 ### 从3个方面入手 现在我们知道了，缓存，作为一个能极大提高用户浏览体验的存在，在我们上网的过程中几乎无时无刻在发生着作用。那么，理解浏览器的缓存机制对于开发同学来说就是非常重要的知识点，这篇文章将会从以下三个方面入手介绍浏览器缓存，以便大家都能构建出自己的一份浏览器缓存体系： 1. 强缓存 2. 协商缓存 3. 缓存位置 ### 1.强缓存 强缓存是一种浏览器缓存的策略，它指的是：**如果缓存资源有效，则直接使用缓存资源，不必再向服务器发起请求。** #### 设置强缓存的两种方式： ##### HTTP/1.0使用Expires `Expires`即过期时间，存在于服务端返回的**HTTP请求响应头**中，告诉浏览器在这个过期时间之前可以直接从缓存里面获取数据，无需再次请求。比如下面这样: `Expires: Mon, 04 Mar 2022 04:01:32 GMT` 表示资源在2012年03月04号4点01分过期，过期了就得向服务端发请求。这个方式看上去没什么问题，其实潜藏了一个问题：这个时间是一个**绝对时间**，它是服务器的时间，如果**客户端的时间和服务器端的时间不一致**，或者**用户修改了客户端的时间**，这样就会影响缓存命中的效果。针对这个问题，在HTTP/1.1中，浏览器有了新的解决方案。 ##### HTTP/1.1使用Cache-Control 在HTTP1.1中，新增了一个非常关键的字段：`Cache-Control`。这个字段也是存在于**HTTP请求响应头**中，它和`Expires`本质的不同在于它并没有采用具体的过期时间点这个方式，而是采用过期时长来控制缓存，对应的字段是`max-age`。比如这个例子：`Cache-Control:max-age=3600` 代表这个响应返回后在 3600 秒，也就是一个小时之内可以直接使用缓存。而且不止于此，`Cache-Control`还可以搭配其他不同属性来控制更多更复杂的场景下的缓存，比如： 1. **`public`：客户端和代理服务器都可以缓存**。因为一个请求可能要经过不同的**代理服务器**最后才到达目标服务器，那么结果就是不仅仅浏览器可以缓存数据，中间的任何代理节点都可以进行缓存。 2. `private`：表示响应资源**仅仅只能被获取它的浏览器端**缓存。它不允许任何中间者缓存响应的资源。 3. `no-cache`: 跳过当前的强缓存，发送HTTP请求，即直接进入**协商缓存**阶段。 4. `no-store`：非常粗暴，不进行任何形式的缓存。**(注意这里要和`no-cache`区分)** 5. `s-maxage`：与上文提到的max-age类似，这里的“s”代表共享，并且，这个指令一般仅用于控制代理服务器或CDN之类的中间者的缓存。 6. `no-transform`:中间代理有时会改变图片以及文件的格式，从而达到提高性能的效果。`no-transform`指令告诉中间代理不要改变资源的格式。 值得注意的是，当`Expires`和`Cache-Control`同时存在的时候，`Cache-Control`会被优先考虑。 ![缓存2.jpg][3] 那么，假如我们已经不在强缓存要求的缓存时间里了，也就是说**强缓存失效了**，接下来怎么办呢？别急，浏览器还有第二道缓存策略————协商缓存。 ### 协商缓存 协商缓存就是强制缓存失效后，浏览器携带缓存标识**(Tag)**向服务器发起请求，由服务器根据缓存标识**(Tag)**决定是否使用缓存的过程。 #### 设置协商缓存的两种方式： ##### HTTP/1.0使用Last-Mod…

【译】shadow DOM 真的能提高样式性能表现吗？
type: item
date: 2021-10-09T18:11:00+08:00
description: 原文链接Does shadow DOM improve style performance$$^{[1]}$$，翻译在2021年10月9日。本译文也收录在公众号大前端从入门到跑路(https://mp.weixin.qq.com/s/wxYkp_DcxW52YTKhj6P8yA "[大前端从入门到跑路]")。 我的一个简短的回答是：能提高一点。虽然它可能还不足以在一般的 Web APP 中产生很大的差异，但我们值得去理解为什么它能提高样式性能。 首先，让我们回顾一下浏览器的渲染流程 $$^{[2]}$$ (`rendering pipeline`)，以及为什么我们可以推测 `shadow DOM` 能提高样式性能。我们知道，浏览器渲染过程的两个基本部分是 **样式计算** (`style calculation`)和 **布局计算** (`layout calculation`)，或者简称为“样式”和“布局”。前者的工作是：确定哪些 DOM 节点具有哪些样式（基于 CSS），后者则是：确定这些 DOM 节点实际放置在页面上的位置（使用上一步中计算出的样式）。 ![](http://120.25.166.245/usr/uploads/2024/05/3246465073.png) Chrome DevTools 中的性能跟踪，显示了基本的 JavaScript → 样式(Style) → 布局(Layout) → 绘制(Paint)流程 浏览器很复杂，但一般来说，页面上的 DOM 节点和 CSS 规则越多，运行样式和布局步骤所需要的时间就越长。提高此过程性能的一个方法是将工作分解为更小的块，即封装(`encapsulation`)——这是 shadow DOM 能提高样式性能的原因。 对于布局(`Layout`)的封装，我们有 CSS Containment $$^{[3]}$$ 。这个在其他文章 $$^{[4]}$$ 里已经讲过了，这里就不赘述了。可以这么说，我认为有足够的证据表明 `CSS containement`可以提高性能，所以如果你还没有尝试将 `contains: content` 置放在 `UI`里面以查看它是否可以提高布局性能，那么你绝对应该去试一试。 至于样式(`Style`)的封装，我们有一些新奇玩意儿：shadow DOM $$^{[5]}$$ 。就像 `CSS Containment`能提高布局性能一样，`shadow DOM`理论上 应该也能提高样式性能。让我们考虑一下原因： ## 什么是样式计算 如前文所述，样式计算不同于布局计算。布局计算与页面的几何布局有关，而样式计算则更明确地与 CSS 相关。这是采用如下规则的过程： ``` div > button { color: blue; } ``` 还有一个 DOM 树： ``` ``` ...例子如上，我们可以确定 ``应该具有 `color:blue`，因为它的父元素是 ``。粗略地说，这是计算 CSS 选择器（在本例中为 `div > button`）的过程。 现在，在最坏的情况下，这是一个 时间复杂度为 $$O(n * m)$$ 操作，其中 n 是 DOM 节点的数量，m 是 CSS 规则的数量。（即遍历每个 DOM 节点和每个 CSS 规则，弄清楚它们是否相互匹配。）显然，浏览器不会这样做，否则任何大小合适的 Web 应用程序都会变得非常缓慢。浏览器在这方面有很多优化，这也是我们经常建议不要过分担心 CSS 选择器性能的部分原因。（请参阅这篇文章 $$^{[6]}$$ ，了解该主题的最新处理） 也就是说，如果你使用过有着大量 CSS 代码量的代码库，你可能会注意到，在 Chrome 性能配置文件中，样式成本不是零。根据 CSS 的大小或复杂程度，你可能会发现实际上花在样式计算上的时间多于布局计算。因此，研究样式性能并不是完全没有价值的。 ## Shadow DOM 和样式计算 为什么 `shadow DOM` 会提高样式性能？这里再说一次，是因为封装！如果你有一个包含 1,000 个规则的 CSS 文件和一个包含 1,000 个节点的 DOM 树，浏览器在运行前，并不会知道哪些规则适用于哪些节点。即便你使用 CSS Modules $$^{[7]}$$ 、Vue scoped CSS$$^{[8]}$$ 或 Svelte scoped CSS$$^{[9]}$$ 来创作你的 CSS，最终你也只会得到一个隐式映射到 DOM 树的样式表，因此浏览器必须在运行时找出 CSS 和 DOM 树之间的联系（例如使用类或属性选择器）。`Shadow DOM` 则不同。使用 shadow DOM，浏览器不必猜测哪些规则适用于哪些节点——因为它就在…