前言
本篇文章主要介绍加载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 loadComponentB,那么需要实现以下代码:// 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…