React 11 :: 优化加载速度

884 阅读5分钟

前言

本篇文章主要介绍加载React应用程序加载速度的两种方法:服务端加载SSR (Server Side Rendering)Code Split

本文将提供两个Repo:

1. SSR

由于React应用程序属于客户端的应用程序,而且是SPA,所以页面将会在bundle.js加载完成之后被渲染出来,所以就引入了一个问题,如果整个的bundle文件过大,就会导致,页面渲染速度降低。

所以就引入了服务端加载的技术来题提升首屏加载速度,注意,这里说的是首屏,而不是整个应用程序。服务端加载的理念是,第一屏的html代码通过服务端加载,也就是说,用户通过浏览器请求HTML,服务端生成整个HTML DOM,然后发送个浏览器,浏览器负责渲染,然后,之后的逻辑还是由React take-over

大致的流程如下图(图片引用自React SSR 同构入门与原理):

image.png

1.1 SSR的优势

  • 首屏加载速度快

    因为整个html dom由服务端生成,所以不管bundle文件是否下载完成,都可以显示页面。

  • SEO Friendly

    可以单独访问React Routing中的任何URL

  • SSR Demo

    大家可以参考React SSR 同构入门与原理,这篇文章来了解SSR的原理,以及手写一个同构的模型。

1.2 SSR的劣势

  • 代码结构复杂
    • 需要引入服务端框架如express或者koa
    • 需要完全定制webpack,分别打包serverclient端代码
    • 因为server端没有dom对象,需要重构cssclient去掉所有css reference,改由server端加载
  • Different Codebase

    我个人理解,如果需要使用SSR,应该在项目起始阶段就构思好,否则如果开始使用client side rendering,然后migrateserver side rendering是一个非常痛苦的过程。而且整个过程是不可逆的。

    单就去掉所有css refernce就会对整个项目产生非常大的影响。所以推荐使用next.jsgatsby这类的框架。

1.3 解决方案

  • 使用框架生成

    在上面我们已经提到过了,如果使用SSR,推荐使用Next.jsGatsby这类的框架。毕竟站在巨人的肩膀上,会省掉很多不必要的工作量。

  • 使用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

根据Reactbundle原则,同步的代码会放到一个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

    假设我们要在ComponentAlazy 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>
       );
    }
    
    • ComponentAdefault exported
    • ComponentBComponentAlazy import, 因为import会返回一个promise对象,所以按照Reactbundle原则,ComponentAComponentB会被bundle到不同的文件中。
    • 调用ComponentAlazy对象时,一定要用Suspend节点包裹住要lazy loadComponent
  • 基于RoutingCode 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中,我们将以前的translationitems两个Component拆分到了不同的bundle中,并在路由中lazy load.

    • 没做code splitting时的文件加载列表

      image.png

    • 做了code splitting时的文件加载列表

      image.png

      可以看到,resources明显少了15K,因为有两个componentbundle文件没有在第一页加载。

    • 运行效果

      lazy-load-demo.gif

      可以看到,只有点击加载了相应的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);
       //...
   },
   // ...
}

运行效果

uglify.gif

所有的cssjs文件都已经被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文件生成。

    image.png

2.2.3 Consume gzip file via Nginx

生成了上面的gz文件之后,需要修改NginxConfiguration,让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

运行效果:

image.png

可以看到原本450Kresrouces只在网络上传输了132K, 还是比较有效的。加载时间也缩短了将近1s。

CompressionDemo Repogitlab.com/yafeya/reac…