性能优化之关键渲染路径

2022-08-25 15:26:34 浏览数 (1)

大家好,我是「柒八九」

今天,我们来谈谈,浏览器的「关键渲染路径」。针对浏览器的一些其他文章,我们前面有介绍。分别从浏览器架构最新的渲染引擎介绍了关于页面渲染的相关概念。对应连接如下。

  • 页面是如何生成的(宏观角度)
  • Chromium 最新渲染引擎--RenderingNG
  • RenderingNG中关键数据结构及其角色

而今天的主角是关键渲染路径Critical Rendering Path。它是影响页面在「加载」阶段的主要标准。

这里再啰嗦一点,通常一个页面有「三个阶段」

  1. 「加载阶段」
    • 是指从「发出请求到渲染出完整页面」的过程
    • 影响到这个阶段的主要因素有「网络」「JavaScript 脚本」
  2. 「交互阶段」
    • 主要是从页面加载完成到「用户交互」的整个过程
    • 影响到这个阶段的主要因素是 「JavaScript 脚本」
  3. 「关闭阶段」
    • 主要是用户发出关闭指令后页面所做的一些「清理操作」

好了,时间不早了。开干。

你能所学到的知识点

  1. 关键渲染路径的各种指标
  2. 关键资源Critical Resource:所有可能「阻碍页面渲染」的资源
  3. 关键路径长度Critical Path Length:获取构建页面所需的所有关键资源所需的 「RTT」(Round Trip Time)
  4. 关键字节Critical Bytes:作为完成和构建页面的一部分而传输的「字节总数」
  5. 重温HTTP缓存
  6. 针对关键渲染路径进行各种优化处理
  7. 针对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树」

下面的代码有三个区域:headermainfooter。并且style.css外部文件

代码语言:javascript复制
<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样式。

代码语言:javascript复制
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 实现了 ECMAScriptAdobe ActionScript 也实现 ECMAScript

上面的内容只是做一个知识点的补充,我们这篇文章中出现的JS还是一般意义上的含义:即javascript文本信息。

JavaScript 是一种用来操作DOM的语言」。这些「操作花费时间」,并增加网站的整体加载时间。所有,

JavaScript 代码被称为 解析器阻塞Parser Blocking资源。 ❞

什么是「解析器阻塞」?当需要「下载」「执行」JavaScript代码时,浏览器会「暂停执行和构建DOM树」。当JavaScript代码被执行完后,DOM树的构建才继续进行。

所以才有, JavaScript是一种昂贵的资源」的说法。


示例演示

下面是一段HTML代码的演示结果,显示了一些文字和图片。正如你所看到的,「整个页面的显示只花了大约40ms」。即使有一张图片,页面显示的时间也更短。这是因为在进行第一次绘制时,「图像没有被当作关键资源」

记住,

❝关键渲染路径Critical Rendering Path都是「关于HTMLCSSJavascript的」

现在,在这段代码中添加css。正如下图所示,一个「额外的请求被触发」了。尽管加载html文件的时间减少了,但处理和显示页面的总体时间却增加了近10倍。为什么呢?

  • 普通的HTML并不涉及太多的资源获取解析工作。但是,「对于CSS文件,必须构建一个CSSOM」HTMLDOMCSSCSSOM 都必须被构建。这无疑是一个耗时的过程。
  • JavaScript 很有可能会查询 CSSOM。这意味着,「在执行任何JavaScript之前,CSS文件必须被完全下载和解析」

「注意」domContentLoadedHTML DOM「完全解析和加载时被触发」。该事件不会等待image、子frame甚至是样式表被完全加载。「唯一的目标是文档被加载」。可以在window中添加事件,以查看DOM是否被解析和加载。

代码语言:javascript复制
window.addEventListener('DOMContentLoaded', (event) => {
    console.log('DOM被解析且加载成功');
});

即使你选择用内联脚本取代外部文件,性能也不会有大的改变。主要是因为需要构建CSSOM。如果你考虑使用外部脚本,可以添加 async属性。这将解除对解析器的阻断

关键路径相关术语

  • 关键资源Critical Resource:所有可能「阻碍页面渲染」的资源
  • 关键路径长度Critical Path Length:获取构建页面所需的所有关键资源所需的 「RTT」(Round Trip Time)
    • 由于渲染引擎有一个「预解析的线程」,在接收到 HTML 数据之后,预解析线程会「快速扫描 HTML 数据中的关键资源」,一旦扫描到了,会立马发起请求
    • 可以认为 JavaScriptCSS「同时发起请求」的,所以它们的「请求是重叠的」,计算它们的 RTT 时,「只需要计算体积最大的那个数据」就可以了
    • 首先是请求 HTML 资源,假设大小是 6KB,小于 14KB,所以 1 个 RTT 就可以解决
    • 它是网络中一个重要的性能指标表示从发送端发送数据开始,到发送端收到来自接收端的确认,「总共经历的时延」
    • 当使用 TCP 协议传输一个文件时,由于 TCP 的特性,这个数据并不是一次传输到服务端的,而是需要拆分成一个个数据包来回多次进行传输的
    • RTT 就是这里的「往返时延」
    • 通常 1 个 HTTP 的数据包在 14KB 左右
    • 至于 JavaScriptCSS 文件
  • 关键字节Critical Bytes:作为完成和构建页面的一部分而传输的「字节总数」

在我们的第一个例子中,如果是普通的HTML脚本,上面各个指标的值如下

  • 1个关键资源(html)
  • 1个RTT
  • 192字节的数据

在第二个例子中,一个普通的HTML和外部CSS脚本,上面各个指标的值如下

  • 2个关键资源(html css)
  • 2个RTT
  • 400字节的数据

如果你希望优化任何框架中的关键渲染路径,你需要在上述指标上下功夫并加以改进。

  • 优化关键资源
    • JavaScriptCSS 改成内联的形式 (性能提升不是很大)
    • 如果 JavaScript 代码没有 DOM 或者 CSSOM 的操作,则可以改成 sync 或者 defer 属性
    • 首屏内容可以优先加载,非首屏内容采用「滚动加载」
  • 优化关键路径长度
    • 「压缩」 CSSJavaScript 资源
    • 移除 HTMLCSSJavaScript 文件中一些「注释内容」
  • 优化关键字节
  • 通过减少关键资源的「个数」和减少关键资源的「大小」搭配来实现
  • 使用 CDN 来减少每次 RTT 时长

减少渲染器阻塞资源

懒加载

加载的关键是 "懒加载"。任何媒体资源、CSSJavaScript、图像、甚至HTML都可以被懒加载。每次加载「有限的页面的内容」,可以提高关键渲染路径。

  • 不要在加载页面时加载这个整个页面的 CSSJavaScriptHTML
  • 相反,可以为一个button添加一个事件监听,只有在用户点击按钮时才加载脚本。
  • 使用Webpack来完成懒加载功能。

这里有一些利用纯JavaScript实现懒加载的技术。

比如,现在又一个<img/>/<iframe/> 在这些情况下,我们可以利用<img><iframe>标签「附带的默认loading属性」。当浏览器看到这个标签时,它会「推迟加载」iframeimage。具体语法如下:

代码语言:javascript复制
<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()方法的返回值,如果没有根元素(即直接相对于视口滚动),则返回null
  • boundingClientRect:目标元素的矩形区域的信息
  • intersectionRect:目标元素与视口(或根元素)的交叉区域的信息
  • intersectionRatio:目标元素的可见比例,即intersectionRectboundingClientRect的比例,「完全可见时为1,完全不可见时小于等于0」

  • 我们观察所有具有.lazy类的元素。
  • 当具有.lazy类的元素在视口上时,「相交率会降到零以下」。如果相交率为零或低于零,说明目标不在视口内。而且,不需要做什么。
代码语言:javascript复制
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

「注意」AsyncDefer 是用于外部脚本的属性。

使用Async处理脚本

当使用 Async 时,将允许浏览器在下载 JavaScript 资源时做其他事情。「一旦下载完成」,下载的JavaScript资源将被执行。

  1. JavaScript「异步下载」的。
  2. 所有其他脚本的执行将被暂停。
  3. DOM渲染将同时发生。
  4. 「DOM渲染将只在脚本执行时暂停」
  5. 渲染阻塞的JavaScript问题可以使用async属性来解决。

「如果一个资源不重要,甚至不要使用async,完全省略它」

代码语言:javascript复制
<p>...执行脚本之前,能看到的内容...</p>

<script>
  document.addEventListener('DOMContentLoaded', () => alert("DOM 被构建完成!"));
</script>

<script async src=""></script>

<p>...上述脚本执行完,才能看到此内容 ...</p>

使用Defer处理脚本

当使用Defer时,JavaScript 资源将在HTML渲染时被下载。然而,「执行不会在脚本被下载后立即发生。相反,它会等待HTML文件被完全渲染」

  1. 脚本的执行只发生在渲染完成之后。
  2. Defer 可以使你的JavaScript资源绝对不会阻断渲染
代码语言:javascript复制
<p>...执行脚本之前,能看到的内容...</p>

<script defer src=""></script>

<p>...此内容不被js所阻塞,也就是说能立即看到...</p>

使用Prelaod处理外部资源

当使用Preload时,它被用于HTML文件中没有的文件,但在渲染或解析JavaScript或CSS文件的时候。有了Preload,浏览器就会下载资源,在资源可用的时候就会执行。

  • 使用Prelaod。浏览器会下载文件,即使它在你的页面上是不必要的。
  • 太多的预载会使你的页面速度下降。
  • 当有太多的预载文件时,使用预载的固有优先权将受到影响。
  • 「只有在首屏页面需要的文件才可以预载」
  • 预载文件会在其他文件被渲染时才会被发现。例如,你在一个CSS文件内添加一个字体的引用。在CSS文件被解析之前,对字体的存在不会被知道。如果该字体被提前下载,它将提高你的网站速度。
  • 「预加载只用于<link>标签」
代码语言:javascript复制
<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-MatchIf-None-MatchIf-Range 字段将这个标识告知服务器,这样服务器就知道该请求和上次的响应是相关的。 这个字段的「功能和 Cookie 是相同」的,但 Cookie 是网景(Netscape)公司自行开发的规格,而 Etag 是将其进行标准化后的规格 ❞

Expires 和 Cache-control:max-age=x(强缓存)

ExpiresCache-control:max-age=x「强制缓存」策略的关键信息,两者均是「响应首部信息」(后端返给客户端)的。 ❞

ExpiresHTTP 1.0 加入的特性,通过指定一个「明确的时间点」作为缓存资源的过期时间,在此时间点之前客户端将使用本地缓存的文件应答请求,而不会向服务器发出实体请求。

Expires 的优点:

  • 可以在缓存过期时间内「减少」客户端的 HTTP 请求
  • 节省了客户端处理时间和提高了 Web 应用的执行速度
  • 减少了「服务器负载」以及客户端网络资源的消耗

对应的语法

代码语言:javascript复制
Expires: <http-date>

<http-date>是一个 HTTP-日期 时间戳

代码语言:javascript复制
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-maxagemax-age类似,这里的「s」代表共享,这个指令一般仅用于 CDNs 或者其他「中间者」(intermediary caches)。这个指令会「覆盖」max-ageexpires响应头。
  • no-transform 中间代理有时会改变图片以及文件的格式,从而达到提高性能的效果。no-transform指令告诉中间代理不要改变资源的格式

max-age 指定的是缓存的「时间跨度」,而非缓存失效的时间点,不会受到客户端与服务器时间误差的影响。 ❞

Expires 相比, max-age 可以「更精确地控制缓存」,并且比 Expires 有「更高的优先级」

强制缓存策略下( Cache-control 未指定 no-cacheno-store)的缓存判断流程

EtagIf-None-Match (协商缓存)

Etag「服务器」为资源分配的字符串形式「唯一性标识」,作为「响应首部」信息返回给浏览器 ❞

「浏览器」Cache-control 指定 no-cache 或者 max-ageExpires 均过期之后,将Etag 值通过 If-None-Match 作为「请求首部」信息发送给服务器。

「服务器」接收到请求之后,对比所请求资源的 Etag 值是否改变,如果未改变将返回 304 Not Modified,并且根据既定的缓存策略分配新的 Cache-control 信息;如果资源发生了改变,则会 返回「最新」的资源以及重新分配Etag值。

如果强制浏览器使用协商缓存策略,需要将 Cache-control 首部信息设置为 no-cache ,这样便不会判断 max-ageExpires 过期时间,从而「每次资源请求都会经过服务器对比」

JS层面做缓存处理(ServerWorker)

在纯JavaScript中,你可以自由地利用service workers来决定是否需要加载数据。例如,我有两个文件:style.cssscript.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 应用中的优化处理

优化被分成两个阶段。

  1. 在应用程序被加载之前
  2. 第二阶段是在应用加载后进行优化

阶段一(加载前)

让我们建立一个简单的应用程序,有如下的结构。

  • 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.jsRoute组件对 React进行 Lazy加载处理。

我们把代码按页面逻辑进行区分。只有当应用程序需要时,才会加载这些逻辑片段。因此,代码的整体重量保持较低。

例如,如果Sidebar组件只有在用户登录时才会被加载,我们有几个方法来提高我们的应用程序的性能。

首先,我们可以在「路由层面」对代码进行懒加载处理。如下面代码所示,代码被分成了三个逻辑块。「只有当用户选择了一个特定的路由时,每个块才会被加载」。这意味着,我们的DOM在初始绘制时不必将 Sidarbar 代码作为其 「Critical Bytes」的一部分。

代码语言:javascript复制
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「条件渲染」机制。

代码语言:javascript复制
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 允许我们在点击按钮的情况下也能加载组件。

代码语言:javascript复制
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/>,或者一条消息,告诉用户为什么页面还没有被画出来。

代码语言:javascript复制
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中实现。当你这样做时,stateprops之间会发生「浅对比」。因此,重新渲染的几率大大降低。

利用React.Memo

  • React.Memo接收组件,并将props记忆化。当一个组件需要重新渲染时,会进行「浅对比」。由于性能原因,这种方法被广泛使用。
代码语言:javascript复制
function MyComponent(props) {
}
function areEqual(prevProps, nextProps) {
  //对比nextProps和prevProps,如果相同,返回false,不会发生渲染
  // 如果不相同,则进行渲染
}
export default React.memo(MyComponent, areEqual);
  • 如果使用函数组件,请使用useCallback()useMemo()

后记

「分享是一种态度」

参考资料:

  • 关键渲染路径
  • 网络拾遗之Http缓存
  • React官网

0 人点赞