大家好,我是「柒八九」。
今天,我们来谈谈,浏览器的「关键渲染路径」。针对浏览器的一些其他文章,我们前面有介绍。分别从浏览器架构和最新的渲染引擎介绍了关于页面渲染的相关概念。对应连接如下。
- 页面是如何生成的(宏观角度)
- Chromium 最新渲染引擎--RenderingNG
- RenderingNG中关键数据结构及其角色
而今天的主角是关键渲染路径Critical Rendering Path。它是影响页面在「加载」阶段的主要标准。
这里再啰嗦一点,通常一个页面有「三个阶段」
- 「加载阶段」
- 是指从「发出请求到渲染出完整页面」的过程
- 影响到这个阶段的主要因素有「网络」和 「JavaScript 脚本」
- 「交互阶段」
- 主要是从页面加载完成到「用户交互」的整个过程
- 影响到这个阶段的主要因素是 「JavaScript 脚本」
- 「关闭阶段」
- 主要是用户发出关闭指令后页面所做的一些「清理操作」
好了,时间不早了。开干。
你能所学到的知识点
❝
- 关键渲染路径的各种指标
- 关键资源Critical Resource:所有可能「阻碍页面渲染」的资源
- 关键路径长度Critical Path Length:获取构建页面所需的所有关键资源所需的 「RTT」(Round Trip Time)
- 关键字节Critical Bytes:作为完成和构建页面的一部分而传输的「字节总数」。
- 重温HTTP缓存
- 针对关键渲染路径进行各种优化处理
- 针对
React
应用做优化处理
❞
1. 加载阶段关键数据
文档对象模型Document Object Model
❝「DOM」:是
HTML
页面在解析后,基于对象的表现形式。 ❞
DOM是一个应用编程接口(API),通过创建表示文档的树,以一种「独立于平台和语言」的方式访问和修改一个页面的内容和结构。
在 HTML
文档中,Web开发者可以使用JS
来CRUD DOM 结构,其主要的目的是「动态」改变HTML文档的结构。
❝「DOM 将整个
HTML
页面抽象为一组分层节点」 ❞
「DOM 并非只能通过 JS 访问」, 像可伸缩矢量图SVG、数学标记语言MathML和同步多媒体集成语言SMIL都增加了该语言独有的 DOM
方法和接口。
「一旦HTML被解析,就会建立一个DOM树」。
下面的代码有三个区域:header
、main
和footer
。并且style.css
为外部文件。
<html>
<head>
<link rel="stylesheet" href="style.css">
<title>关键渲染路径示例</title>
<body>
<header>
<h1>...</h1>
<p>...</p>
</header>
<main>
<h1>...</h1>
<p>...</p>
</main>
<footer>
<small>...</small>
</footer>
</body>
</head>
</html>
当上述 HTML
代码被浏览器解析为 DOM树
状结构时,其各个节点的关系如下。
DOM树
每个浏览器都「需要一些时间解析HTML」。并且,「清晰的语义标记」有助于减少浏览器解析HTML所需的时间。(不完整或者错误的语义标记,还需要浏览器根据上下文去分析和判断)
具体,浏览器是如何将HTML
字符串信息,转换成能够被JS操作的DOM
对象,不在此文的讨论范围内。不过,我们可以举一个很小的例子。在我们JS算法探险之栈(Stack)中,有一个题就是如何判断括号的正确性。
❝给定一个只包括 '(',')','{','}','[',']' 的字符串 s ,判断字符串是否有效。有效字符串需满足: 左括号必须用相同类型的右括号闭合。 左括号必须以正确的顺序闭合。 示例: 输入:s = "()[]{}" 输出:true 输入:s = "(]" 输出:false ❞
其实,上面的例子就是最简单的一种标签匹配。或者说的稳妥点,它们的主要思想是一致的。
CSSOM Tree
❝
CSSOM
也是一个基于对象的树。它「负责处理与DOM树相关的样式」。 ❞
承接上文,我们这里有和上面HTML
配套的CSS
样式。
header{
background-color: white;
color: black;
}
p{
font-weight:400;
}
h1{
font-size:72px;
}
small{
text-align:left
}
对于上述CSS声明,CSSOM树
将显示如下。
CSSOM树
由于,css
的部分属性能够被「继承」,所以,在父级节点定义的属性,如果满足情况,子节点也是会有对应的属性信息,最后将对应的样式信息,渲染到页面上。
❝一般来说,CSS被认为是一种阻断渲染Render-Blocking资源。 ❞
什么是「渲染阻断」?渲染阻塞资源是一个组件,它将「不允许浏览器渲染整个DOM树,直到给定的资源被完全加载」。
CSS
是一种渲染阻断资源,因为在CSS完全加载之前,你无法渲染树。
起初,页面中所有CSS
信息都被存放在一个文件中 。现在,开发人员通过一些技术手段,能够将CSS
文件「分割」开来,「只在渲染的早期阶段提供关键样式」。
执行JS
先将一个小知识点,其实,在前面的文章中,我们已经讲过了。这里,我们再啰嗦一遍。
在「浏览器环境下」,JS = ECMAScript DOM BOM
。
ECMAScript
JS的「核心部分」,即 ECMA-262 定义的语言,并不局限于 Web 浏览器。
Web
浏览器只是 ECMAScript
实现可能存在的一种宿主环境Host Environment。而宿主环境提供 ECMAScript
的「基准实现」和与「环境自身交互必需的扩展」。(比如 DOM
使用 ECMAScript
核心类型和语法,提供特定于环境的额外功能)。
像我们比较常见的Web 浏览器、 Node.js和已经被淘汰的 Adobe Flash都是ECMA
的宿主环境。
ECMAScript
只是对实现ECMA-262规范的一门语言的称呼, JS
实现了 ECMAScript
,Adobe ActionScript
也实现 ECMAScript
。
上面的内容只是做一个知识点的补充,我们这篇文章中出现的JS
还是一般意义上的含义:即javascript
文本信息。
「JavaScript
是一种用来操作DOM
的语言」。这些「操作花费时间」,并增加网站的整体加载时间。所有,
❝
JavaScript
代码被称为 解析器阻塞Parser Blocking资源。 ❞
什么是「解析器阻塞」?当需要「下载」和「执行」JavaScript
代码时,浏览器会「暂停执行和构建DOM树」。当JavaScript代码被执行完后,DOM树的构建才继续进行。
所以才有, 「JavaScript
是一种昂贵的资源」的说法。
示例演示
下面是一段HTML
代码的演示结果,显示了一些文字和图片。正如你所看到的,「整个页面的显示只花了大约40ms」。即使有一张图片,页面显示的时间也更短。这是因为在进行第一次绘制时,「图像没有被当作关键资源」。
记住,
❝关键渲染路径Critical Rendering Path都是「关于
HTML
、CSS
和Javascript
的」 ❞
现在,在这段代码中添加css
。正如下图所示,一个「额外的请求被触发」了。尽管加载html
文件的时间减少了,但处理和显示页面的总体时间却增加了近10倍。为什么呢?
- 普通的
HTML
并不涉及太多的资源获取和解析工作。但是,「对于CSS文件,必须构建一个CSSOM」。HTML
的DOM
和CSS
的CSSOM
都必须被构建。这无疑是一个耗时的过程。 JavaScript
很有可能会查询CSSOM
。这意味着,「在执行任何JavaScript之前,CSS文件必须被完全下载和解析」。
「注意」:domContentLoaded
在HTML DOM
被「完全解析和加载时被触发」。该事件不会等待image
、子frame
甚至是样式表被完全加载。「唯一的目标是文档被加载」。可以在window
中添加事件,以查看DOM是否被解析和加载。
window.addEventListener('DOMContentLoaded', (event) => {
console.log('DOM被解析且加载成功');
});
即使你选择用内联脚本取代外部文件,性能也不会有大的改变。主要是因为需要构建CSSOM
。如果你考虑使用外部脚本,可以添加 async
属性。这将解除对解析器的阻断。
关键路径相关术语
- 关键资源Critical Resource:所有可能「阻碍页面渲染」的资源
- 关键路径长度Critical Path Length:获取构建页面所需的所有关键资源所需的 「RTT」(Round Trip Time)
- 由于渲染引擎有一个「预解析的线程」,在接收到
HTML
数据之后,预解析线程会「快速扫描HTML
数据中的关键资源」,一旦扫描到了,会立马发起请求 - 可以认为
JavaScript
和CSS
是「同时发起请求」的,所以它们的「请求是重叠的」,计算它们的 RTT 时,「只需要计算体积最大的那个数据」就可以了 - 首先是请求
HTML
资源,假设大小是6KB
,小于14KB
,所以 1 个 RTT 就可以解决 - 它是网络中一个重要的性能指标表示从发送端发送数据开始,到发送端收到来自接收端的确认,「总共经历的时延」
- 当使用
TCP
协议传输一个文件时,由于TCP
的特性,这个数据并不是一次传输到服务端的,而是需要拆分成一个个数据包来回多次进行传输的 RTT
就是这里的「往返时延」- 通常 1 个
HTTP
的数据包在14KB
左右 - 至于
JavaScript
和CSS
文件
- 由于渲染引擎有一个「预解析的线程」,在接收到
- 关键字节Critical Bytes:作为完成和构建页面的一部分而传输的「字节总数」。
在我们的第一个例子中,如果是普通的HTML脚本,上面各个指标的值如下
- 1个关键资源(
html
) - 1个RTT
- 192字节的数据
在第二个例子中,一个普通的HTML和外部CSS脚本,上面各个指标的值如下
- 2个关键资源(
html
css
) - 2个RTT
- 400字节的数据
如果你希望优化任何框架中的关键渲染路径,你需要在上述指标上下功夫并加以改进。
❝
- 优化关键资源
- 将
JavaScript
和CSS
改成内联的形式 (性能提升不是很大) - 如果
JavaScript
代码没有DOM
或者CSSOM
的操作,则可以改成sync
或者defer
属性 - 首屏内容可以优先加载,非首屏内容采用「滚动加载」
- 将
- 优化关键路径长度
- 「压缩」
CSS
和JavaScript
资源 - 移除
HTML
、CSS
、JavaScript
文件中一些「注释内容」
- 「压缩」
- 优化关键字节
- 通过减少关键资源的「个数」和减少关键资源的「大小」搭配来实现
- 使用
CDN
来减少每次RTT
时长
❞
减少渲染器阻塞资源
懒加载
加载的关键是 "懒加载"。任何媒体资源、CSS
、JavaScript
、图像、甚至HTML
都可以被懒加载。每次加载「有限的页面的内容」,可以提高关键渲染路径。
- 不要在加载页面时加载这个整个页面的
CSS
、JavaScript
和HTML
。 - 相反,可以为一个
button
添加一个事件监听,只有在用户点击按钮时才加载脚本。 - 使用
Webpack
来完成懒加载功能。
这里有一些利用纯JavaScript实现懒加载的技术。
比如,现在又一个<img/>/<iframe/>
在这些情况下,我们可以利用<img>
和<iframe>
标签「附带的默认loading
属性」。当浏览器看到这个标签时,它会「推迟加载」iframe
和image
。具体语法如下:
<img src="image.png" loading="lazy">
<iframe src="abc.html" loading="lazy"></iframe>
注意:loading=lazy
的懒加载不应该用在非滚动视图上。
不能利用loading=lazy
的浏览器中,你可以使用IntersectionObserver
。这个API设置了一个根,并为每个元素的可见性配置了根的比率。当一个元素在视口中是可见的,它就会被加载。
❝
IntersectionObserverEntry
对象提供目标元素的信息,一共有六个属性。每个属性的含义如下。
time
:可见性发生变化的时间,是一个高精度时间戳,单位为毫秒target
:被观察的目标元素,是一个DOM
节点对象rootBounds
:根元素的矩形区域的信息,getBoundingClientRect()
方法的返回值,如果没有根元素(即直接相对于视口滚动),则返回nullboundingClientRect
:目标元素的矩形区域的信息intersectionRect
:目标元素与视口(或根元素)的交叉区域的信息intersectionRatio
:目标元素的可见比例,即intersectionRect
占boundingClientRect
的比例,「完全可见时为1,完全不可见时小于等于0」
❞
- 我们观察所有具有
.lazy
类的元素。 - 当具有
.lazy
类的元素在视口上时,「相交率会降到零以下」。如果相交率为零或低于零,说明目标不在视口内。而且,不需要做什么。
var intersectionObserver = new IntersectionObserver(function(entries) {
if (entries[0].intersectionRatio <= 0) return;
//intersection ratio 在0上,说明在视口上能看到
console.log('进行加载处理');
});
// 针对目标DOM进行处理
intersectionObserver.observe(document.querySelector('.lazy));
Async, Defer, Preload
「注意」:Async
和 Defer
是用于外部脚本的属性。
使用Async处理脚本
当使用 Async
时,将允许浏览器在下载 JavaScript
资源时做其他事情。「一旦下载完成」,下载的JavaScript
资源将被执行。
JavaScript
是「异步下载」的。- 所有其他脚本的执行将被暂停。
- DOM渲染将同时发生。
- 「DOM渲染将只在脚本执行时暂停」。
- 渲染阻塞的JavaScript问题可以使用
async
属性来解决。
「如果一个资源不重要,甚至不要使用async,完全省略它」
代码语言:javascript复制<p>...执行脚本之前,能看到的内容...</p>
<script>
document.addEventListener('DOMContentLoaded', () => alert("DOM 被构建完成!"));
</script>
<script async src=""></script>
<p>...上述脚本执行完,才能看到此内容 ...</p>
使用Defer处理脚本
当使用Defer
时,JavaScript
资源将在HTML渲染时被下载。然而,「执行不会在脚本被下载后立即发生。相反,它会等待HTML文件被完全渲染」。
- 脚本的执行只发生在渲染完成之后。
Defer
可以使你的JavaScript资源绝对不会阻断渲染
<p>...执行脚本之前,能看到的内容...</p>
<script defer src=""></script>
<p>...此内容不被js所阻塞,也就是说能立即看到...</p>
使用Prelaod处理外部资源
当使用Preload
时,它被用于HTML文件中没有的文件,但在渲染或解析JavaScript或CSS文件的时候。有了Preload
,浏览器就会下载资源,在资源可用的时候就会执行。
- 使用
Prelaod
。浏览器会下载文件,即使它在你的页面上是不必要的。 - 太多的预载会使你的页面速度下降。
- 当有太多的预载文件时,使用预载的固有优先权将受到影响。
- 「只有在首屏页面需要的文件才可以预载」。
- 预载文件会在其他文件被渲染时才会被发现。例如,你在一个
CSS
文件内添加一个字体的引用。在CSS文件被解析之前,对字体的存在不会被知道。如果该字体被提前下载,它将提高你的网站速度。 - 「预加载只用于
<link>
标签」。
<link rel="preload" href="style.css" as="style">
<link rel="preload" href="main.js" as="script">
编写原生(Vanilla) JS,避免使用第三方脚本
原生 JS拥有很好的性能和可访问性。对于一个特定的用例,你不需要全盘的依赖第三方脚本。虽然这些库往往能解决一堆问题,但是依靠沉重的库来解决简单的问题会导致你的代码性能下降。
「我们的要求不是避免使用框架和编写100%的新代码。我们的要求是使用辅助函数和小规模的插件。」
缓存Caching和失效Expiring内容
如果资源在你的页面上被反复使用,那么一直加载它们将是一种折磨。这类似于每次都在加载网站。缓存将有助于防止这种循环。在HTTP响应头中给内容提供过期信息,只有在它们过期时才加载。
HTTP缓存
我们之前在网络拾遗之Http缓存就介绍过,关于http缓存的知识点,我就直接拿来主义了。
❝「最好最快」的请求就是「没有请求」 ❞
浏览器对「静态资源」的缓存本质上是 HTTP
协议的缓存策略,其中又可以分为「强制缓存」和「协商缓存」。
❝两种缓存策略都会「将资源缓存到本地」 ❞
- 强制缓存策略根据「过期时间」决定使用本地缓存还是请求新资源:
- 协商缓存每次都会「发出请求」,经过「服务器进行对比」后决定采用本地缓存还是新资源。
具体采用哪种缓存策略,由 HTTP 协议的首部( Headers
)信息决定。
在网络通信之生成HTTP消息中我们介绍过,消息头按照用途可分为「四大类」 1. 通用头:适用于请求和响应的头字段 2. 请求头:用于表示请求消息的附加信息的头字段 3. 响应头:用于表示响应消息的附加信息的头字段 4. 实体头:用于「消息体」的附加信息的头字段
我们对HTTP缓存用到的字段进行一次简单的分类和汇总。
头字段 | 所属分组 |
---|---|
Expires | 实体头 |
Cache-control | 通用头 |
ETag | 实体头 |
❝ETag: 在「更新操作」中,有时候需要基于「上一次请求的响应数据」来发送下一次请求。在这种情况下,这个字段可以用来提供上次响应与下次请求之间的「关联信息」。上次响应中,服务器会通过
Etag
向客户端发送一个唯一标识,在下次请求中客户端可以通过If-Match
、If-None-Match
、If-Range
字段将这个标识告知服务器,这样服务器就知道该请求和上次的响应是相关的。 这个字段的「功能和 Cookie 是相同」的,但 Cookie 是网景(Netscape)公司自行开发的规格,而 Etag 是将其进行标准化后的规格 ❞
Expires 和 Cache-control:max-age=x(强缓存)
❝
Expires
和Cache-control:max-age=x
是「强制缓存」策略的关键信息,两者均是「响应首部信息」(后端返给客户端)的。 ❞
Expires
是 HTTP 1.0
加入的特性,通过指定一个「明确的时间点」作为缓存资源的过期时间,在此时间点之前客户端将使用本地缓存的文件应答请求,而不会向服务器发出实体请求。
Expires
的优点:
- 可以在缓存过期时间内「减少」客户端的 HTTP 请求
- 节省了客户端处理时间和提高了 Web 应用的执行速度
- 减少了「服务器负载」以及客户端网络资源的消耗
对应的语法
代码语言:javascript复制Expires: <http-date>
<http-date>
是一个 HTTP-日期 时间戳
Expires: Wed, 24 Oct 2022 14:00:00 GMT
上述信息指定对应资源的「缓存过期时间」为 2022年8月24日 14点
❝
Expires
一个「致命的缺陷」是:它所指定的时间点是以「服务器为准」的时间,但是「客户端进行过期判断」时是将「本地的时间与此时间点对比」。 ❞
如果客户端的时间与服务器存在「误差」,比如服务器的时间是 2022年 8月 23日 13 点
,而客户端的时间是 2022年 8月 23日 15 点
,那么通过 Expires
控制的缓存资源将会「失效」,客户端将会发送实体请求获取对应资源。
针对这个问题, HTTP 1.1
新增了 Cache-control
首部信息以便「更精准」地控制缓存。
常用的 Cache-control 信息有以下几种。
no-cache
: 使用ETag
响应头来告知客户端(浏览器、代理服务器)这个资源首先需要被检查是否在服务端修改过,在这之前不能被复用。这个「意味着no-cache将会和服务器进行一次通讯」,确保返回的资源没有修改过,如果没有修改过,才没有必要下载这个资源。反之,则需要重新下载。no-store
在处理资源不能被缓存和复用的逻辑的时候与no-cache
类似。然而,他们之间有一个重要的「区别」。no-store
要求资源每次都被请求并且下载下来。当在处理隐私信息(private information)的时候,这是一个重要的特性。public & private
public
表示此响应可以被浏览器以及中间缓存器「无限期缓存」,此信息并不常用,常规方案是使用max-age
指定精确的缓存时间private
表示此响应可以被用户浏览器缓存,但是「不允许任何中间缓存器」对其进行缓存。例如,用户的浏览器可以缓存包含用户私人信息的 HTML 网页,但CDN
却不能缓存。max-age=<seconds>
指定从「请求的时刻」开始计算,此响应的缓存副本有效的最长时间(单位:「秒」) 例如,max-age=360
表示浏览器在接下来的 1 小时内使用此响应的本地缓存,不会发送实体请求到服务器s-maxage=<seconds>
s-maxage
与max-age
类似,这里的「s」代表共享,这个指令一般仅用于CDNs
或者其他「中间者」(intermediary caches)。这个指令会「覆盖」max-age
和expires
响应头。no-transform
中间代理有时会改变图片以及文件的格式,从而达到提高性能的效果。no-transform
指令告诉中间代理不要改变资源的格式
❝
max-age
指定的是缓存的「时间跨度」,而非缓存失效的时间点,不会受到客户端与服务器时间误差的影响。 ❞
与 Expires
相比, max-age
可以「更精确地控制缓存」,并且比 Expires 有「更高的优先级」
强制缓存策略下( Cache-control
未指定 no-cache
和no-store
)的缓存判断流程
Etag
和 If-None-Match
(协商缓存)
❝
Etag
是「服务器」为资源分配的字符串形式「唯一性标识」,作为「响应首部」信息返回给浏览器 ❞
「浏览器」在 Cache-control
指定 no-cache
或者 max-age
和 Expires
均过期之后,将Etag
值通过 If-None-Match
作为「请求首部」信息发送给服务器。
「服务器」接收到请求之后,对比所请求资源的 Etag
值是否改变,如果未改变将返回 304 Not Modified
,并且根据既定的缓存策略分配新的 Cache-control
信息;如果资源发生了改变,则会 返回「最新」的资源以及重新分配的 Etag
值。
如果强制浏览器使用协商缓存策略,需要将 Cache-control
首部信息设置为 no-cache
,这样便不会判断 max-age
和 Expires
过期时间,从而「每次资源请求都会经过服务器对比」。
JS层面做缓存处理(ServerWorker)
在纯JavaScript中,你可以自由地利用service workers
来决定是否需要加载数据。例如,我有两个文件:style.css
和 script.js
。我需要加载这些文件,我可以使用service workers
来决定这些资源是否必须保持最新,或者可以使用缓存。
在Web性能优化之Worker线程(上)我们有介绍过关于ServerWork
的详细介绍。如果感兴趣,可以去瞅瞅。
当用户第一次启动单页应用程序时,安装将被执行。
代码语言:javascript复制self.addEventListener('install', function(event) {
event.waitUntil(
caches.open(cacheName).then(function(cache) {
return cache.addAll(
[
'styles.css',
'script.js'
]
);
})
);
});
当用户执行一项操作时
代码语言:javascript复制document.querySelector('.lazy').addEventListener('click', function(event) {
event.preventDefault();
caches.open('lazy_posts’).then(function(cache) {
fetch('/get-article’).then(function(response) {
return response;
}).then(function(urls) {
cache.addAll(urls);
});
});
});
处理网络请求
代码语言:javascript复制self.addEventListener('fetch', function(event) {
event.respondWith(
caches.open('lazy_posts').then(function(cache) {
return cache.match(event.request).then(function (response) {
return response
});
})
);
});
纸上得来终觉浅,绝知此事要躬行。道理,都懂,我们来看看在实际开发中,如何做优化处理。我们按React
开发为例子。
React 应用中的优化处理
优化被分成两个阶段。
- 在应用程序被加载之前
- 第二阶段是在应用加载后进行优化
阶段一(加载前)
让我们建立一个简单的应用程序,有如下的结构。
Header
Sidebar
Footer
代码结构如下。
代码语言:javascript复制webpack-demo
|- package.json
|- package-lock.json
|- webpack.config.js
|- /dist
|- /src
|- index.js
|- Header.js
|- Sidebar.js
|- Footer.js
|- loader.js
|- route.js
|- /node_modules
在我们的应用程序中,只有当用户登录时,才应该看到侧边栏。Webpack
是一个很好的工具,可以帮助我们进行「代码拆分」。如果我们启用了代码拆分,我们可以从App.js
或Route
组件对 React
进行 Lazy加载处理。
我们把代码按页面逻辑进行区分。只有当应用程序需要时,才会加载这些逻辑片段。因此,代码的整体重量保持较低。
例如,如果Sidebar
组件只有在用户登录时才会被加载,我们有几个方法来提高我们的应用程序的性能。
首先,我们可以在「路由层面」对代码进行懒加载处理。如下面代码所示,代码被分成了三个逻辑块。「只有当用户选择了一个特定的路由时,每个块才会被加载」。这意味着,我们的DOM在初始绘制时不必将 Sidarbar
代码作为其 「Critical Bytes」的一部分。
import {
Switch,
browserHistory,
BrowserRouter as Router,
Route
} from 'react-router-dom';
const Header = React.lazy( () => import('Header'));
const Footer = React.lazy( () => import('Footer'));
const Sidebar = React.lazy( () => import('Sidebar'));
const Routes = (props) => {
return isServerAvailable ? (
<Router history={browserHistory}>
<Switch>
<Route path="/" exact><Redirect to='/Header' /></Route>
<Route path="/sidebar" exact component={props => <Sidebar {...props} />} />
<Route path="/footer" exact component={props => <Footer {...props} />} />
</Switch>
</Router>
}
同样地,我们也可以「从父级App.js中实现懒加载」。这利用了React
的「条件渲染」机制。
const Header = React.lazy( () => import('Header'));
const Footer = React.lazy( () => import('Footer'));
const Sidebar = React.lazy( () => import('Sidebar'));
function App (props) {
return(
<React.Fragment>
<Header user = {props.user} />
{props.user ? <Sidebar user = {props.user /> : null}
<Footer/>
</React.Fragment>
)
}
谈到条件渲染,React
允许我们在点击按钮的情况下也能加载组件。
import _ from 'lodash';
function buildSidebar() {
const element = document.createElement('div');
const button = document.createElement('button');
button.innerHTML = '登录';
element.innerHTML = _.join(['加载 Sidebar', 'webpack'], ' ');
element.appendChild(button);
button.onclick = e =>
import(/* webpackChunkName: "sidebar" */ './sidebar')
.then(module => {
const sidebar = module.default;
sidebar()
});
return element;
}
document.body.appendChild(buildSidebar());
在实践中,重要的是「把所有的路由或组件写在在叫做Suspense的组件中」,以懒加载的方式加载。Suspense
的作用是在懒加载的组件被加载时,为应用程序提供一个「后备内容」。后备内容可以是任何东西,比如一个<Loader/>
,或者一条消息,告诉用户为什么页面还没有被画出来。
import React, { Suspense } from 'react';
import {
Switch,
browserHistory,
BrowserRouter as Router,
Route
} from 'react-router-dom';
import Loader from ‘./loader.js’
const Header = React.lazy( () => import('Header'));
const Footer = React.lazy( () => import('Footer'));
const Sidebar = React.lazy( () => import('Sidebar'));
const Routes = (props) => {
return isServerAvailable ? (
<Router history={browserHistory}>
<Suspense fallback={<Loader trigger={true} />}>
<Switch>
<Route path="/" exact><Redirect to='/Header' /></Route>
<Route path="/sidebar" exact component={props => <Sidebar {...props} />} />
<Route path="/footer" exact component={props => <Footer {...props} />} />
</Switch>
</Suspense>
</Router>
}
阶段二
现在,应用程序已经完全加载,接下来就到了调和阶段了。其中的所有的处理逻辑都是React
为我们代劳。其中最重要的一点就是React-Fiber
机制。
如果想了解React_Fiber,可以参考我们之前的文章。
使用正确的状态管理方法
- 每当
React DOM树
被修改时,它都会「迫使浏览器回流」。这将对你的应用程序的性能产生严重影响。「调和被用来确保减少重新流转的次数」。同样地,React使用状态管理来防止重现。例如,你有一个useState()
hook。 - 如果使用的是类组件,利用
shouldComponentUpdate()
生命周期方法。shouldComponentUpdate()
必须在PureComponent
中实现。当你这样做时,state
和props
之间会发生「浅对比」。因此,重新渲染的几率大大降低。
利用React.Memo
React.Memo
接收组件,并将props
记忆化。当一个组件需要重新渲染时,会进行「浅对比」。由于性能原因,这种方法被广泛使用。
function MyComponent(props) {
}
function areEqual(prevProps, nextProps) {
//对比nextProps和prevProps,如果相同,返回false,不会发生渲染
// 如果不相同,则进行渲染
}
export default React.memo(MyComponent, areEqual);
- 如果使用函数组件,请使用
useCallback()
和useMemo()
。
后记
「分享是一种态度」。
参考资料:
- 关键渲染路径
- 网络拾遗之Http缓存
- React官网