React 16 加载性能优化


React 16 加载性能优化



一次页面加载的全过程

1.用户打开页面,这个时候页面是完全空白的;

2.然后 html 和引用的 css 加载完毕,浏览器进行首次渲染,我们把首次渲染需要加载的资源体积称为 “首屏体积”

3.然后 react、react-dom、业务代码加载完毕,应用第一次渲染,或者说首次内容渲染

4.然后应用的代码开始执行,拉取数据、进行动态import、响应事件等等,完毕后页面进入可交互状态;

5.接下来 lazyload 的图片等多媒体内容开始逐渐加载完毕;

6.然后直到页面的其它资源(如错误上报组件、打点上报组件等)加载完毕,整个页面的加载就结束了。

7.接下来,我们就分别讨论这些步骤中,有哪些值得优化的点。


一. 打开页面 -> 首屏

写过 React 或者任何 SPA 的你,一定知道目前几乎所有流行的前端框架(React、Vue、Angular),它们的应用启动方式都是极其类似的:

1.html 中提供一个 root 节点

2. 把应用挂载到这个节点上

这样的模式,使用 webpack 打包之后,一般就是三个文件:

一个体积很小、除了提供个 root 节点以外的没什么卵用的html(大概 1-4 KB

一个体积很大的 js(50 - 1000 KB 不等

一个 css 文件(当然如果你把 css 打进 js 里了,也可能没有)

这样造成的直接后果就是,用户在 50 - 1000 KB 的 js 文件加载、执行完毕之前,页面是 完全空白的

也就是说,这个时候:

首屏体积(首次渲染需要加载的资源体积) = html + js + css

1.1. 在 root 节点中写一些东西

我们完全可以把首屏渲染的时间点提前,比如在你的 root 节点中写一点东西:

或者我们可以在把 root 节点内的内容画得好看一些:

1.2. 使用 html-webpack-plugin 自动插入 loading

实际业务中肯定是有很多很多页面的,每个页面都要我们手动地复制粘贴这么一个 loading 态显然太不优雅了,这时我们可以考虑使用 html-webpack-plugin 来帮助我们自动插入 loading。

然后在模板中引用即可:

1.3. 使用 prerender-spa-plugin 渲染首屏

在一些比较大型的项目中,Loading 可能本身就是一个 React/Vue 组件,在不做服务器端渲染的情况下,想把一个已经组件化的 Loading 直接写入 html 文件中会很复杂,不过依然有解决办法。

prerender-spa-plugin 是一个可以帮你在构建时就生成页面首屏 html 的一个 webpack 插件,原理大致如下:

指定 dist 目录和要渲染的路径

插件在 dist 目录中开启一个静态服务器,并且使用无头浏览器(puppeteer)访问对应的路径,执行 JS,抓取对应路径的 html。

把抓到的内容写入 html,这样即使没有做服务器端渲染,也能达到跟服务器端渲染几乎相同的作用(不考虑动态数据的话)

1.4. 除掉外链 css

截止到目前,我们的首屏体积 = html + css,依然有优化的空间,那就是把外链的 css 去掉,让浏览器在加载完 html 时,即可渲染首屏。

有人可能要质疑,把 css 打入 js 包里,会丢失浏览器很多缓存的好处(比如你只改了 js 代码,导致构建出的 js 内容变化,但连带 css 都要一起重新加载一次),这样做真的值得吗?

确实这么做会让 css 无法缓存,但按照目前组件化的开发模式,缓存不应该在 js/css 这个维度上区分,而是应该按照“组件”区分,即配合动态 import 缓存组件。


二. 首屏 -> 首次内容渲染

这一段过程中,浏览器主要在做的事情就是加载、运行 JS 代码,所以如何提升 JS 代码的加载、运行性能,就成为了优化的关键。

几乎所有业务的 JS 代码,都可以大致划分成以下几个大块:

基础框架,如 React、Vue 等,这些基础框架的代码是不变的,除非升级框架;

Polyfill,对于使用了 ES2015+ 语法的项目来说,为了兼容性,polyfill 是必要的存在;

业务基础库,业务的一些通用的基础代码,不属于框架,但大部分业务都会使用到;

业务代码,特点是具体业务自身的逻辑代码。

想要优化这个时间段的性能,也就是要优化上面四种资源的加载速度。

2.1. 缓存基础框架

基础框架代码的特点就是必需不变,是一种非常适合缓存的内容。

所以我们需要做的就是为基础框架代码设置一个尽量长的缓存时间,使用户的浏览器尽量通过缓存加载这些资源。

附:HTTP 缓存资源小结

HTTP 为我们提供了很好几种缓存的解决方案,不妨总结一下:

1. expires

expires: Thu, 16 May 2019 03:05:59 GMT

在 http 头中设置一个过期时间,在这个过期时间之前,浏览器的请求都不会发出,而是自动从缓存中读取文件,除非缓存被清空,或者强制刷新。缺陷在于,服务器时间和用户端时间可能存在不一致,所以 HTTP/1.1 加入了 cache-control 头来改进这个问题。

2. cache-control

cache-control: max-age=31536000

设置过期的时间长度(秒),在这个时间范围内,浏览器请求都会直接读缓存。当 expires 和 cache-control 都存在时,cache-control 的优先级更高。

3. last-modified / if-modified-since

这是一组请求/相应头

响应头:

last-modified: Wed, 16 May 2018 02:57:16 GMT

请求头:

if-modified-since: Wed, 16 May 2018 05:55:38 GMT

服务器端返回资源时,如果头部带上了 last-modified,那么资源下次请求时就会把值加入到请求头 if-modified-since中,服务器可以对比这个值,确定资源是否发生变化,如果没有发生变化,则返回 304。

4. etag / if-none-match

这也是一组请求/相应头

响应头:

etag: "D5FC8B85A045FF720547BC36FC872550"

请求头:

if-none-match: "D5FC8B85A045FF720547BC36FC872550"

原理类似,服务器端返回资源时,如果头部带上了 etag,那么资源下次请求时就会把值加入到请求头 if-none-match 中,服务器可以对比这个值,确定资源是否发生变化,如果没有发生变化,则返回 304。


2.2. 使用 SplitChunksPlugin 自动拆分业务基础库

Webpack 4 抛弃了原有的 CommonChunksPlugin,换成了更为先进的 SplitChunksPlugin,用于提取公用代码。

它们的区别就在于,CommonChunksPlugin 会找到多数模块中都共有的东西,并且把它提取出来(common.js),也就意味着如果你加载了 common.js,那么里面可能会存在一些当前模块不需要的东西。

而 SplitChunksPlugin 采用了完全不同的 heuristics 方法,它会根据模块之间的依赖关系,自动打包出很多很多(而不是单个)通用模块,可以保证加载进来的代码一定是会被依赖到的。

下面是一个简单的例子,假设我们有 4 个 chunk,分别依赖了以下模块:

如果是以前的 CommonChunksPlugin,那么默认配置会把它们打包成下面这样:

显然在这里,react、react-dom、angular 这些公用的模块没有被抽出成为独立的包,存在进一步优化的空间。

现在,新的 SplitChunksPlugin 会把它们打包成以下几个包:

这就保证了所有公用的模块,都会被抽出成为独立的包,几乎完全避免了多页应用中,重复加载相同模块的问题。

具体如何配置 SplitChunksPlugin,请参考webpack官方文档。


三、首次内容渲染 -> 可交互

这一段过程中,浏览器主要在做的事情就是加载及初始化各项组件

3.1. Code Splitting

大多数打包器(比如 webpack、rollup、browserify)的作用就是把你的页面代码打包成一个很大的 “bundle”,所有的代码都会在这个 bundle 中。但是,随着应用的复杂度日益提高,bundle 的体积也会越来越大,加载 bundle 的时间也会变长,这就对加载过程中的用户体验造成了很大的负面影响。

为了避免打出过大的 bundle,我们要做的就是切分代码,也就是 Code Splitting,目前几乎所有的打包器都原生支持这个特性。

Code Splitting 可以帮你“懒加载”代码,以提高用户的加载体验,如果你没办法直接减少应用的体积,那么不妨尝试把应用从单个 bundle 拆分成单个 bundle + 多份动态代码的形式。

比如我们可以把下面这种形式:

改写成动态 import 的形式,让首次加载时不去加载 math 模块,从而减少首次加载资源的体积。

React Loadable 是一个专门用于动态 import 的 React 高阶组件,你可以把任何组件改写为支持动态 import 的形式。

上面的代码在首次加载时,会先展示一个 loading-component,然后动态加载 my-component 的代码,组件代码加载完毕之后,便会替换掉 loading-component。

3.2. 编译到 ES2015+ ,提升代码运行效率

如今大多数项目的做法都是,编写 ES2015+ 标准的代码,然后在构建时编译到 ES5 标准运行。

比如一段非常简洁的 class 语法:

会被编译成这样:

但实际上,大部分现代浏览器已经原生支持 class 语法,比如 iOS Safari 从 2015 年的 iOS 9.0 开始就支持了,根据 caniuse 的数据,目前移动端上 90% 用户的浏览器都是原生支持 class 语法的,其它 ES2015 的特性也是同样的情况。

也就是说,在当下 2018 年,对于大部分用户而言,我们根本不需要把代码编译到 ES5,不仅体积大,而且运行速度慢。我们需要做的,就是把代码编译到 ES2015+,然后为少数使用老旧浏览器的用户保留一个 ES5 标准的备胎即可。

具体的解决方法就是 <script type="module"> 标签。

支持 <script type="module"> 的浏览器,必然支持下面的特性:

1.async/await

2.Promise

3.Class

4.箭头函数、Map/Set、fetch 等等...

而不支持 <script type="module"> 的老旧浏览器,会因为无法识别这个标签,而不去加载 ES2015+ 的代码。另外老旧的浏览器同样无法识别 nomodule 熟悉,会自动忽略它,从而加载 ES5 标准的代码。

简单地归纳为下图:


四、可交互 -> 内容加载完毕

这个阶段就很简单了,主要是各种多媒体内容的加载

4.1. LazyLoad

懒加载其实没什么好说的,目前也有一些比较成熟的组件了,自己实现一个也不是特别难:

react-lazyload

react-lazy-load

当然你也可以实现像 Medium 的那种加载体验(好像知乎已经是这样了),即先加载一张低像素的模糊图片,然后等真实图片加载完毕之后,再替换掉。

实际上目前几乎所有 lazyload 组件都不外乎以下两种原理:

监听 window 对象或者父级对象的 scroll 事件,触发 load;

使用 Intersection Observer API 来获取元素的可见性。

4.2. placeholder

我们在加载文本、图片的时候,经常出现“闪屏”的情况,比如图片或者文字还没有加载完毕,此时页面上对应的位置还是完全空着的,然后加载完毕,内容会突然撑开页面,导致“闪屏”的出现,造成不好的体验。

为了避免这种突然撑开的情况,我们要做的就是提前设置占位元素,也就是 placeholder:

已经有一些现成的第三方组件可以用了:

react-placeholder

react-hold


五、总结

这篇文章里,我们一共提到了下面这些优化加载的点:

1.在 HTML 内实现 Loading 态或者骨架屏;

2.去掉外联 css;

3.缓存基础框架;

4.使用 SplitChunksPlugin 拆分公共代码;

5.使用动态 import,切分页面代码,减小首屏 JS 体积;

6.编译到 ES2015+,提高代码运行效率,减小体积;

7.使用 lazyload 和 placeholder 提升加载体验。

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