前言
本篇文章主要介绍加载React
应用程序加载速度的两种方法:服务端加载SSR (Server Side Rendering)
与Code Split
。
本文将提供两个Repo:
-
Code Splitting
介绍如何拆分
bundle
,以及lazy load
。 -
Compression
介绍如何将
bundle
文件压缩,用来进一步优化加载速度。
1. SSR
由于React
应用程序属于客户端的应用程序,而且是SPA
,所以页面将会在bundle.js
加载完成之后被渲染出来,所以就引入了一个问题,如果整个的bundle
文件过大,就会导致,页面渲染速度降低。
所以就引入了服务端加载的技术来题提升首屏加载速度,注意,这里说的是首屏,而不是整个应用程序。服务端加载的理念是,第一屏的html
代码通过服务端加载,也就是说,用户通过浏览器请求HTML
,服务端生成整个HTML DOM
,然后发送个浏览器,浏览器负责渲染,然后,之后的逻辑还是由React
take-over
。
大致的流程如下图(图片引用自React SSR 同构入门与原理):
1.1 SSR
的优势
- 首屏加载速度快
因为整个
html dom
由服务端生成,所以不管bundle
文件是否下载完成,都可以显示页面。 SEO Friendly
可以单独访问
React Routing
中的任何URL
。SSR Demo
大家可以参考React SSR 同构入门与原理,这篇文章来了解SSR的原理,以及手写一个同构的模型。
1.2 SSR
的劣势
- 代码结构复杂
- 需要引入服务端框架如
express
或者koa
- 需要完全定制
webpack
,分别打包server
与client
端代码 - 因为
server
端没有dom
对象,需要重构css
,client
去掉所有css reference
,改由server
端加载
- 需要引入服务端框架如
Different Codebase
我个人理解,如果需要使用
SSR
,应该在项目起始阶段就构思好,否则如果开始使用client side rendering
,然后migrate
到server side rendering
是一个非常痛苦的过程。而且整个过程是不可逆的。单就去掉所有
css refernce
就会对整个项目产生非常大的影响。所以推荐使用next.js
或gatsby
这类的框架。
1.3 解决方案
-
使用框架生成
在上面我们已经提到过了,如果使用
SSR
,推荐使用Next.js
或Gatsby
这类的框架。毕竟站在巨人的肩膀上,会省掉很多不必要的工作量。 -
使用
Client Side Rendering
另一种解决方案,就是还是使用
Client Side Rendering
,不过需要解决加载速度和SEO Friendly
的问题。加载速度问题可以通过
code splitting
+Compression
的方式部分解决。Code Splitting
的作用是将不同的component
放到不同的bundle中,在显示component
时再加载相应的bundle
文件。Compression
则是将bundle
文件通过压缩算法压缩,以减小网络的传输量。SEO Friendly
的问题,我们之前介绍过,可以通过customize webpack
的方式解决,有兴趣的小伙伴,可以查看我这篇文章。
2. Code Splitting
+ Compression
2.1 Code Splitting
根据React
的bundle
原则,同步的代码会放到一个bundle
文件中,而异步的代码会放到不同的bundle
文件中。我们来看如下的代码:
// bundle之后会放到同一个bundle文件中
import { add } from './math';
console.log(add(16, 26));
// bundle之后会放到两个bundle文件中
import("./math").then(math => {
console.log(math.add(16, 26));
});
-
lazy load component
假设我们要在
ComponentA
中lazy load
ComponentB
,那么需要实现以下代码:// ComponentA.tsx const ComponentA: React.FC = () => { return <div>Component A</div> } export default ComponentA;
// ComponentB.tsx import { Suspense, lazy } from 'react'; // assume that ComponentA.tsx & ComponentB.tsx are in the same folder. const ComponentA = lazy(() => import('./ComponentA')); const ComponentB: React.FC = () => { return ( <div> <Suspense fallback={<div>Loading...</div>}> <ComponentA /> </Suspense> </div> ); }
ComponentA
被default exported
- 在
ComponentB
中ComponentA
被lazy import
, 因为import
会返回一个promise
对象,所以按照React
的bundle
原则,ComponentA
和ComponentB
会被bundle
到不同的文件中。 - 调用
ComponentA
的lazy
对象时,一定要用Suspend
节点包裹住要lazy load
的Component
。
-
基于
Routing
的Code Splitting
其实,对于
Code Splitting
来说,最主要的应用场景还是React Routing
。因为在Routing
的场景下,不是所有的Component
都是在首屏加载出来的,所以,只有Routing
到相应的Component
,再加载Component
对应的bundle
是一个比较不错的选择。可以参考我给出的
Demo repo
来实现Code Splitting based on React Routing
.Demo Repo
: gitlab.com/yafeya/reac…// Router.tsx const TranslationWrapper = lazy(() => import("../i18n/translation")); const ItemsWrapper = lazy(() => import("../ItemList/ItemList")); export const Router = () => { return ( <BrowserRouter> <Suspense fallback={<div>loading...</div>}> <Switch> <Route exact={true} path="/" component={Home} /> <Route path="/redux" component={Redux} /> <Route path="/items" component={ItemsWrapper} /> <Route path="/item/:id" component={ItemDetailWrapper} /> <Route path="/i18n" component={TranslationWrapper} /> <Route component={() => <Redirect to="/" />} /> </Switch> </Suspense> </BrowserRouter> ); }
在这个Demo中,我们将以前的
translation
和items
两个Component
拆分到了不同的bundle中,并在路由中lazy load
.-
没做
code splitting
时的文件加载列表 -
做了
code splitting
时的文件加载列表可以看到,resources明显少了
15K
,因为有两个component
的bundle
文件没有在第一页加载。 -
运行效果
可以看到,只有点击加载了相应的
Component
对应的bundle
文件才被加载到了系统中。
-
2.2 Compression
Compression
是指将bundle
文件压缩,以减小传输的网络带宽,也能起到优化加载速度的作用。Compression
有两种,一种是build
阶段的compression
, 另一种是在运行阶段的compression
。由于运行时的compression
非常依赖于运行的web-server
的实现方式,所以不太通用,这里只介绍build
阶段的压缩。
2.2.1 Uglify bundle file
在介绍压缩之前,我们先来介绍必不可少的一步,也就是代码的uglify
,这一步通常是将js
代码的变量和函数名称混淆,这样起到代码保护的作用。
npm i -D react-app-rewire-uglifyjs
// config-overrides.js
module.exports = {
webpack: function (config, env) {
//...
const rewireUglifyjs = require('react-app-rewire-uglifyjs');
config = rewireUglifyjs(config);
//...
},
// ...
}
运行效果
所有的css
与js
文件都已经被uglified
.
2.2.2 Compression in build procedure
-
安装
compression
包react-app-rewire-compression-plugin
-
Customize Webpack
// config-overrides.js module.exports = { webpack: function (config, env) { //... const rewireCompressionPlugin = require('react-app-rewire-compression-plugin'); config = rewireCompressionPlugin(config, env, { test: /\.js$|\.css$|\.html$/, cache: true, threshold: 10240, minRatio: 0.8 }); //... }, // ... }
-
build
结果build
之后,可以看到static
目录下会有很多gz
文件生成。
2.2.3 Consume gzip file via Nginx
生成了上面的gz
文件之后,需要修改Nginx
的Configuration
,让web-server
能够使用生成的gz文件。
server {
# ...
gzip on;
gzip_static on;
gzip_types text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript;
gzip_proxied any;
gzip_vary on;
gzip_comp_level 6;
gzip_buffers 16 8k;
gzip_http_version 1.1;
# ...
}
# 发布网站
./docker-exec
运行效果:
可以看到原本
450K
的resrouces
只在网络上传输了132K
, 还是比较有效的。加载时间也缩短了将近1s。
Compression
的Demo Repo
:gitlab.com/yafeya/reac…