性能优化之关键渲染路径

不要再问“那怎么可能”,而是问“为什么不能”

大家好,我是柒八九

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

而今天的主角是<span style="font-weight:800;color:#FFA500;font-size:18px">{关键渲染路径| Critical Rendering Path}</span>。它是影响页面在加载阶段的主要标准。

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

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

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

你能所学到的知识点

  1. 关键渲染路径的各种指标
  2. <span style="font-weight:800;color:#FFA500;font-size:18px">{关键资源| Critical Resource}</span>:所有可能阻碍页面渲染的资源
  3. <span style="font-weight:800;color:#FFA500;font-size:18px">{关键路径长度|Critical Path Length}</span>:获取构建页面所需的所有关键资源所需的 RTT(Round Trip Time)
  4. <span style="font-weight:800;color:#FFA500;font-size:18px">{关键字节| Critical Bytes}</span>:作为完成和构建页面的一部分而传输的字节总数
  5. 重温HTTP缓存
  6. 针对关键渲染路径进行各种优化处理
  7. 针对React应用做优化处理

1. 加载阶段关键数据

<span style="font-weight:800;color:#FFA500;font-size:18px">{文档对象模型| Document Object Model}</span>

DOM:是HTML页面在解析后,基于对象的表现形式。

DOM是一个应用编程接口(API),通过创建表示文档的树,以一种独立于平台和语言的方式访问和修改一个页面的内容和结构。

HTML 文档中,Web开发者可以使用JS来CRUD DOM 结构,其主要的目的是动态改变HTML文档的结构。

DOM 将整个HTML页面抽象为一组分层节点

DOM 并非只能通过 JS 访问, 像<span style="font-weight:700;color:green;">{可伸缩矢量图| SVG}</span>、<span style="font-weight:700;color:green;">{数学标记语言| MathML}</span>和<span style="font-weight:700;color:green;">{同步多媒体集成语言| SMIL}</span>都增加了该语言独有的 DOM 方法和接口。

一旦HTML被解析,就会建立一个DOM树

下面的代码有三个区域:headermainfooter。并且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被认为是一种<span style="font-weight:800;color:#FFA500;font-size:18px">{阻断渲染| Render-Blocking}</span>资源。

什么是渲染阻断?渲染阻塞资源是一个组件,它将不允许浏览器渲染整个DOM树,直到给定的资源被完全加载
CSS 是一种渲染阻断资源,因为在CSS完全加载之前,你无法渲染树。

起初,页面中所有CSS信息都被存放在一个文件中 。现在,开发人员通过一些技术手段,能够将CSS文件分割开来,只在渲染的早期阶段提供关键样式


执行JS

先将一个小知识点,其实,在前面的文章中,我们已经讲过了。这里,我们再啰嗦一遍。

浏览器环境下JS = ECMAScript + DOM + BOM

ECMAScript

JS的核心部分,即 ECMA-262 定义的语言,并不局限于 Web 浏览器。

Web 浏览器只是 ECMAScript 实现可能存在的一种<span style="font-weight:800;color:#FFA500;font-size:18px">{宿主环境| Host Environment}</span>。而宿主环境提供 ECMAScript基准实现和与环境自身交互必需的扩展。(比如 DOM 使用 ECMAScript 核心类型和语法,提供特定于环境的额外功能)。

像我们比较常见的Web 浏览器Node.js和已经被淘汰的 Adobe Flash都是ECMA的宿主环境。

ECMAScript 只是对实现ECMA-262规范的一门语言的称呼, JS 实现了 ECMAScriptAdobe ActionScript 也实现 ECMAScript

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


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

JavaScript 代码被称为 <span style="font-weight:800;color:#FFA500;font-size:18px">{解析器阻塞| Parser Blocking}</span>资源。

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

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


示例演示

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

记住,

<span style="font-weight:800;color:#FFA500;font-size:18px">{关键渲染路径| Critical Rendering Path}</span>都是关于HTMLCSSJavascript

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

  • 普通的HTML并不涉及太多的资源获取解析工作。但是,对于CSS文件,必须构建一个CSSOMHTMLDOMCSSCSSOM 都必须被构建。这无疑是一个耗时的过程。

  • JavaScript 很有可能会查询 CSSOM。这意味着,在执行任何JavaScript之前,CSS文件必须被完全下载和解析

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

window.addEventListener('DOMContentLoaded', (event) => {
    console.log('DOM被解析且加载成功');
});

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


关键路径相关术语

  • <span style="font-weight:800;color:#FFA500;font-size:18px">{关键资源| Critical Resource}</span>:所有可能阻碍页面渲染的资源

  • <span style="font-weight:800;color:#FFA500;font-size:18px">{关键路径长度|Critical Path Length}</span>:获取构建页面所需的所有关键资源所需的 RTT(Round Trip Time)

    • 当使用 TCP 协议传输一个文件时,由于 TCP 的特性,这个数据并不是一次传输到服务端的,而是需要拆分成一个个数据包来回多次进行传输的
    • RTT 就是这里的往返时延
      • 它是网络中一个重要的性能指标表示从发送端发送数据开始,到发送端收到来自接收端的确认,总共经历的时延
    • 通常 1 个 HTTP 的数据包在 14KB 左右
      • 首先是请求 HTML 资源,假设大小是 6KB,小于 14KB,所以 1 个 RTT 就可以解决
    • 至于 JavaScriptCSS 文件
      • 由于渲染引擎有一个预解析的线程,在接收到 HTML 数据之后,预解析线程会快速扫描 HTML 数据中的关键资源,一旦扫描到了,会立马发起请求
      • 可以认为 JavaScriptCSS同时发起请求的,所以它们的请求是重叠的,计算它们的 RTT 时,只需要计算体积最大的那个数据就可以了
  • <span style="font-weight:800;color:#FFA500;font-size:18px">{关键字节| Critical Bytes}</span>:作为完成和构建页面的一部分而传输的字节总数

在我们的第一个例子中,如果是普通的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。具体语法如下:

<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类的元素在视口上时,相交率会降到零以下。如果相交率为零或低于零,说明目标不在视口内。而且,不需要做什么。
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,完全省略它

<p>...执行脚本之前,能看到的内容...</p>

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

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

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

使用Defer处理脚本

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

  1. 脚本的执行只发生在渲染完成之后。
  2. 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%的新代码。我们的要求是使用辅助函数和小规模的插件。


<span style="font-weight:800;color:#FFA500;font-size:18px">{缓存| Caching}</span>和<span style="font-weight:800;color:#FFA500;font-size:18px">{失效| Expiring}</span>内容

如果资源在你的页面上被反复使用,那么一直加载它们将是一种折磨。这类似于每次都在加载网站。缓存将有助于防止这种循环。在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 应用的执行速度
  • 减少了服务器负载以及客户端网络资源的消耗

对应的语法

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-maxagemax-age类似,这里的s代表共享,这个指令一般仅用于 CDNs 或者其他中间者(intermediary caches)。这个指令会覆盖max-ageexpires响应头。

  • no-transform
    中间代理有时会改变图片以及文件的格式,从而达到提高性能的效果。no-transform指令告诉中间代理不要改变资源的格式

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

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

强制缓存策略下( Cache-control 未指定 no-cache
no-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的详细介绍。如果感兴趣,可以去瞅瞅。

当用户第一次启动单页应用程序时,安装将被执行

self.addEventListener('install', function(event) {
  event.waitUntil(
    caches.open(cacheName).then(function(cache) {
      return cache.addAll(
        [
          'styles.css',
          'script.js'
        ]
      );
    })
  );
});

当用户执行一项操作时

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);
    });
  });
});

处理网络请求

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. 在应用程序被加载之前
    1. 第二阶段是在应用加载后进行优化

阶段一(加载前)

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

  • Header
  • Sidebar
  • Footer

代码结构如下。

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的一部分。

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

利用React.Memo

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

后记

分享是一种态度

参考资料:

全文完,既然看到这里了,如果觉得不错,随手点个赞和“在看”吧。

本文由mdnice多平台发布

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,383评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,522评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,852评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,621评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,741评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,929评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,076评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,803评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,265评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,582评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,716评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,395评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,039评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,027评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,488评论 2 361
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,612评论 2 350

推荐阅读更多精彩内容