React资源性能优化

本文将从以下几个方面描述React的资源优化:

1. Code Splitting;
2. externals&CDN;
3. DllPlugin;

版本信息

"webpack": "4.29.6",
"react": "16.8.6",
"react-router": "5.0.0",

一、路由Code Splitting:

  • @loadable/component

One great feature of the web is that we don’t have to make our visitors download the entire app before they can use it. You can think of code splitting as incrementally downloading the app. To accomplish this we’ll use webpack, @babel/plugin-syntax-dynamic-import, and loadable-components.

在react-router@5官方文档中推荐了"@loadable/component"做Code Splitting,具体使用如下:

{
  "presets": ["@babel/preset-react"],
  "plugins": ["@babel/plugin-syntax-dynamic-import"]
}

import loadable from "@loadable/component";
import Loading from "./Loading.js";

const LoadableComponent = loadable(() => import("./Dashboard.js"), {
  fallback: <Loading />
});

export default class LoadableDashboard extends React.Component {
  render() {
    return <LoadableComponent />;
  }
}
  • react的React.lazy+ Suspense

react官网推荐了React.lazy方式进行Code Splitting,具体使用如下:

import { lazy, Suspense } from 'react';

const LoadableComponent = lazy(() => import("./Dashboard.js"));

export default class LoadableDashboard extends React.Component {
  render() {
    return (
      <div>
        <Suspense fallback={<div>Loading...</div>}>
            <LoadableComponent />;
        </Suspense>
      </div>
  )
  }
}

React.lazy 接受一个函数,这个函数需要动态调用 import()。它必须返回一个 Promise,该 Promise 需要 resolve 一个 defalut export 的 React 组件。

然后应在 Suspense 组件中渲染 lazy 组件,如此使得我们可以使用在等待加载 lazy 组件时做优雅降级(如 loading 指示器等)。

  • 自定义方式

其实不管是vue还是react,其路由懒加载的实现得益于wepack的异步模块打包,webpack会对代码中异步引入的模块单独打包一份,直到真正调用的时候才去服务端拿。

const a = () => import('./LoadableComponent')

const a = (r)=>require.ensure([], () => r(require('./LoadableComponent‘)),'chunkname')

以上代码本质是一样的,最终返回一个promise对象,其实就是webpack异步模块打包方法,只有在模块真正调用的时候才会加载。

在vue-router中我们只要在路由配置的component中直接传入() => import('./LoadableComponent')即可。而在react的路由中,它路由配置中的component必须传入react 的component对象。所以需要对返回promise对象进行处理。

  • es6+import+async高阶组件

import('path/to/module') -> Promise

import(
  /* webpackChunkName: "my-chunk-name" */
  /* webpackMode: "lazy" */
  'module'
);

webpackChunkName:新 chunk 的名称。从 webpack 2.6.0 开始,[index] and [request] 占位符,分别支持赋予一个递增的数字和实际解析的文件名。Adding this comment will cause our separate chunk to be named [my-chunk-name].js instead of [id].js.


webpackMode:从 webpack 2.6.0 开始,可以指定以不同的模式解析动态导入。支持以下选项:

"lazy"(默认):为每个 import() 导入的模块,生成一个可延迟加载(lazy-loadable) chunk。
"lazy-once":生成一个可以满足所有 import() 调用的单个可延迟加载(lazy-loadable) chunk。此 chunk 将在第一次 import() 调用时获取,随后的 import() 调用将使用相同的网络响应。注意,这种模式仅在部分动态语句中有意义,例如 import(`./locales/${language}.json`),其中可能含有多个被请求的模块路径。
"eager":不会生成额外的 chunk,所有模块都被当前 chunk 引入,并且没有额外的网络请求。仍然会返回 Promise,但是是 resolved 状态。和静态导入相对比,在调用 import()完成之前,该模块不会被执行。
"weak":尝试加载模块,如果该模块函数已经以其他方式加载(即,另一个 chunk 导入过此模块,或包含模块的脚本被加载)。仍然会返回 Promise,但是只有在客户端上已经有该 chunk 时才成功解析。如果该模块不可用,Promise 将会是 rejected 状态,并且网络请求永远不会执行。当需要的 chunks 始终在(嵌入在页面中的)初始请求中手动提供,而不是在应用程序导航在最初没有提供的模块导入的情况触发,这对于通用渲染(SSR)是非常有用的。

上面说到react的路由中要求必须传入一个react的component对象,然而现在返回的是一个延时的promise对象
,这个时候我们可以考虑在react的生命周期函数上做文章,而最终又需要返回一个react组件,所以我们可以考虑使用高阶组件。具体实现方式如下:

//lazyLoad.js
import React from 'react';

export default function lazyLoad(componentfn) {
    class LazyloadComponent extends React.Component {
        constructor(props) {
            super(props);
            this.state = {
                component: null
            }
        }
        async componentWillMount() {
            const { component} = await componentfn();
            this.setState({component})
        }
        render() {
            const C = this.state.component;
            return C ? <C {...this.props}/> : null;
        }
    }
    return LazyloadComponent;
}

//router.js
import lazyLoad from './lazyLoad'
const a = lazyLoad(() => import("./LoadableComponent"))
  • es6+纯import(高阶函数)

在上面使用了async+await,本质上是返回一个promise对象,在promise返回后加载组件,但是用纯import也是可以实现这个功能的。

//lazyLoad.js
import React from 'react';

export default function lazyLoad(componentfn) {
    class LazyloadComponent extends React.Component {
        constructor(props) {
            super(props);
            this.state = {
                component: null
            }
        }
        componentWillMount() {
             this.load();
        }
       load(){
         componentfn().then((Com)=>{  //组件加载完成时
                this.setState({
                    component:Com.default?Com.default:null
                });
            });
       }
        render() {
            const C = this.state.component;
            return C ? <C {...this.props}/> : null;
        }
    }
    return LazyloadComponent;
}

  • es6+require.ensure(高阶函数)

require.ensure() 是 webpack 特有的,已经被 import() 取代。

require.ensure(
  dependencies: String[],
  callback: function(require),
  errorCallback: function(error),
  chunkName: String
)

require.ensurer虽然已经不推荐使用,但是require.ensure作为原始的懒加载方式,还是可以实现的,具体实现如下:

//lazyLoad.js
import React from 'react';

export default function lazyLoad(componentfn) {
    class LazyloadComponent extends React.Component {
        constructor(props) {
            super(props);
            this.state = {
                component: null
            }
        }
        componentWillMount() {
             this.load();
        }
       load(){
         new Promise((resolve,reject)=>{
                require.ensure([], function(require) {//[]依赖项
                    var c = componentfn().default;
                    resolve(c);
                });
          }).then((data)=>{
                this.setState({
                    Com:data
                });
            });
       }
        render() {
            const C = this.state.component;
            return C ? <C {...this.props}/> : null;
        }
    }
    return LazyloadComponent;
}

//router.js
import lazyLoad from './lazyLoad'
const a = lazyLoad(() => require("./LoadableComponent"))

二、externals&CDN:

externals可以防止将某些import的包打包进bundle中,而是在运行时再去外部(script的方式)获取这些扩展依赖,这样会减少bundle包大小。

先说使用方法,基本上分三步:

- 引入cdn library资源
- 配置 webpack externals
- 文件中引用library
//index.html
<head>
<script crossorigin src="https://unpkg.com/react@16/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.production.min.js"></script>
</head>

// webpack.config.js
module.exports = {
   externals: {
     'react': 'React',// '包名':'全局变量' 
     'react-dom': 'ReactDOM'
   }
}
// 包名react指的是 `import React from 'react'`中的'react'
//全局变量React指的是react暴露出来的全局对象名

//index.js
import React from 'react'

下面引用时webpack官方文档,对externals对说明:

externals 配置选项提供了「从输出的 bundle 中排除依赖」的方法。相反,所创建的 bundle 依赖于那些存在于用户环境(consumer's environment)中的依赖。此功能通常对 library 开发人员来说是最有用的,然而也会有各种各样的应用程序用到它。

防止将某些 import 的包(package)打包到 bundle 中,而是在运行时(runtime)再去从外部获取这些扩展依赖(external dependencies)。

三、DllPlugin & DllReferencePlugin:

DllPlugin结合DllRefrencePlugin插件的运用,对将要产出的bundle文件进行拆解打包,将不需要改动的第三方插件与自己的业务代码进行分开打包,可以很彻底地加快webpack的打包速度,从而在开发过程中极大地缩减构建时间。

DllPlugin这个插件是在一个额外的独立的 webpack 设置中创建一个只有 dll 的 bundle(dll-only-bundle)。 这个插件会生成一个名为 manifest.json 的文件,这个文件是用来让 [DLLReferencePlugin]映射到相关的依赖上去。

DllPlugin的作用就是做了两件小事:根据entry,生成一份vendor.dll文件和生成一份manifest.json文件

DllReferencePlugin 这个插件是在 webpack 主配置文件中设置的, 这个插件把只有 dll 的 bundle(们)(dll-only-bundle(s)) 引用到需要的预编译的依赖。

DllPlugin和DllRefrencePlugin需要配合使用,它们一个负责生成dll文件,一个负责使用dll文件,DllPlugin的需要一个单独的webpack配置文件,这个配置文件告诉webpack-cli应该如何打包这个dll,而DllRefrencePlugin是在主配置文件中使用。

  • 使用步骤
  1. 配置一份webpack配置文件,用于生成动态链接库。

const path = require('path')
const webpack = require('webpack')
const {
  CleanWebpackPlugin
} = require('clean-webpack-plugin')


// dll文件存放的目录
const dllPath = 'public/vendor'

module.exports = {
  entry: {
    vendor: ['antd','react-redux','redux','redux-thunk','axios']
  },
  output: {
    path: path.join(__dirname, dllPath),
    filename: '[name].dll.js',
    library: '[name]_[hash]'
  },
  plugins: [
    new CleanWebpackPlugin(),
    new webpack.DllPlugin({
      path: path.join(__dirname, dllPath, '[name]-manifest.json'),
      name: '[name]_[hash]',
      context: process.cwd()
    })
  ]
}

  1. 使用动态链接库,黄金搭档DllReferencePlugin
  externals:{
      'react': 'React',// '包名':'全局变量' 
      'react-dom': 'ReactDOM'
    },
  plugins: [
      new webpack.DllReferencePlugin({
        context: process.cwd(),
        manifest: require('./public/vendor/vendor-manifest.json')
    }),
  1. 在html中引用dll文件
<body>
    <div id="app"></div>
    <script src="./public/vendor/vendor.dll.js"></script>
</body>

DllPlugin优化,使用于将项目依赖的基础模块(第三方模块)抽离出来,然后打包到一个个单独的动态链接库中。当下一次打包时,通过ReferencePlugin,如果打包过程中发现需要导入的模块存在于某个动态链接库中,就不能再次被打包,而是去动态链接库中get到。

DllPlugin实际上也是属于公共代码提取的范畴,但与CommonsChunkPlugin不一样的是,它不仅仅是把公用代码提取出来放到一个独立的文件供不同的页面来使用,它更重要的一点是:把公用代码和它的使用者(业务代码)从编译这一步就分离出来。

参考文献

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

推荐阅读更多精彩内容

  • 利用 Webpack 来优化 Web 性能属于加载性能优化 的一部分: ☛ Web Performance Opt...
    一个笑点低的妹纸阅读 10,574评论 8 8
  • 一.可视化 webpack 输出文件的大小 添加webpack-bundle-analyzer yarn add ...
    程序人生_小龙阅读 1,078评论 0 0
  • 1. 前言 随着前端项目的不断扩大,一个原本简单的网页应用所引用的js文件可能变得越来越庞大。尤其在近期流行的单页...
    cbw100阅读 2,177评论 2 8
  • 昨天,我和我哥玩电动滑板车,这个滑板车加一个板手和一个闸,主要是这个板手,板手只要一扭就飞快地走了,它的速度...
    f8fdc2b43794阅读 147评论 0 0
  • 当遇到字符串中夹杂网址的时候,我们一般的方法都是用正则的方式来挑出网址的部分,然后把它替换成文字,这样就牵涉到正则...
    Frey丶阅读 1,487评论 4 7