阅读 209

React性能优化之代码分割

代码分割从两方面来讲:React Lazy 和 webpack两方面

每次单页应用打包出的文件又臭又长,是最蛋疼的事,浏览器刷个半天才出来网页让人恨不得找个地洞钻进去,这种事很尴尬,让人怀疑你这前端不专业

所以正题来了,这篇文章主要讲解

  • 如何分模块拆分代码
  • 如何提取公共库,避免重复打包(两个模块都用到jQuery,如何只打包进一个chunk)
  • 如何打包需要的代码(比如一个ui库,你只用到一个Button,那么只打包Button)
  • React组件如何实现懒加载

先讲 React Lazy

先上个最简单的demo,App.js和Child.js

// App.js

import React from 'react';
import Child from './Child';

function App() {
	return (
		<div className="App">
			<Child />
		</div>
	);
}

export default App;
复制代码
// Child.js

import React from 'react';

class Child extends React.Component{
    render(){
        return <div class="childClass">我是子组件啊</div>
    }
}

export default Child;
复制代码

就这个目录结构,然后执行打包,打包之后如下:

可以看到Child组件被打包到了main.chunk.js里面
那么Child是否可以做懒加载呢?

查找React文档,可以找到code split,那么用React lazy来对Child组件做一下懒加载,还是原来的组件App和Child

// App.js

import React, { lazy, Suspense } from 'react';

const Child = lazy(() => import('./Child'))

function App() {
	return (
		<div className="App">
			<Suspense fallback={<div>Loading...</div>}>
				<Child />
			</Suspense>
		</div>
	);
}

export default App;
复制代码
// Child.js

import React from 'react';

class Child extends React.Component{
    render(){
        return <div class="childClass">我是子组件啊</div>
    }
}

export default Child;
复制代码

其实api来说很简单,就两句代码,这就实现了懒加载

import React, { lazy, Suspense } from 'react';

const Child = lazy(() => import('./Child'))
复制代码

可是源码里可以注意到还有一个Suspense,而且不用Suspense的话,浏览器会给出一个提示:

从错误提示来看,lazy非要搭配Suspense使用了。那么Suspense是何方神圣呢?

可以试想一下,既然是懒加载,那么当Child还未加载完成之前,这个视图怎么办?

bingo!Suspense就是为了处理这个的,让视图更友好,为懒加载组件做优雅降级,它叫加载指示器
好的,那已经加上lazy了,执行打包吧

再刷新下页面,发现Child组件被打包到了2.chunk.js里面,说明已经运行成功了。而且在Child加载成功之前,有个loading在提示。good!效果还不错

可是问题来了。

阅读React lazy的官方文档。发现lazy传入一个函数,而且此函数需要返回一个Thenable,那么当没有返回Thenable的时候会怎么办呢?

发现浏览器挂了。。。这只是试下异常情况,所以最好还是按照React的官方文档去操作吧

再从webpack谈下

webpack的代码分离有三种方法:

  • 多个entry
  • 插件来防止重复:SplitChunksPlugin
  • 动态导入
先讲下最高端的,动态导入(以es6 import为🌰)

此章节讲解如何一个ui库,解构导入let { Button } = ui;只导入Button
这也是老生常谈的antd的import { Button } from 'antd',只导入Button问题

先上个反面教材吧

// index.js

import { mainAdd } from './main';
console.log(mainAdd(1, 2));
复制代码
// main.js

const mainAdd = (a, b) => {
    return a + b;
}

const mainMultiple = (a, b) => {
    return a * b;
}

export {
    mainAdd,
    mainMultiple
}
复制代码

webpack.config.js的配置entry就只配置index.js,来webpack打包一把

血崩啊,明明只是import了mainAdd,mainMultiple咋还打包进去了

好的,反面教材讲完了,现在教大家如何修改,先修改一下文件目录

// webpack.config.js

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
	mode: 'development',
	entry: {
		index: './src/index.js'
	},
	output: {
		filename: '[name].bundle.js',
		path: path.resolve(__dirname, 'dist')
	},
	plugins: [
		new HtmlWebpackPlugin()
	],
	module: {
		rules: [
			{
                test: /\.js$/,
				loaders: 'babel-loader',
				include: path.join(__dirname, 'src'),
				options: {
					plugins: [
						["import", { "libraryName": "main", "libraryDirectory": "../../src/main"}],
					]
				},
			}
		]
	}
};
复制代码
//index.js

import { mainAdd } from 'main';
console.log(mainAdd(1, 2));
复制代码
// main目录下的main-add.js

const mainAdd = (a, b) => {
    return a + b;
}
export default mainAdd;
复制代码
// main目录下的main-multiple.js

const mainMultiple = (a, b) => {
    return a * b;
}
export default mainMultiple;
复制代码

好的,再来执行一次打包,发现只打包进了mainAdd,而mainMultiple没有打包进去。

总结一下,主要用到一个babel的知识点

  • 需要用到babel-plugin-import插件。顾名思义,就是个动态import的plugin,然后配置babel-loader的options
options: {
	plugins: [
		["import", { "libraryName": "main", "libraryDirectory": "../../src/main"}],
	]
},
复制代码

libraryName指代的是import { mainAdd } from 'main'这里的main -> 库名,libraryDirectory是main下面的哪个文件夹。比如antd/lib/button,这里libraryDirectory配置的是lib,这里默认是指向node_modules下的。我是放在src/main目录下,所以这里配置为"../../src/main"

webpack Entry多入口

这个就不多说了,大家用的都比较多

webpack 公共库拆分打包

还是老习惯,先来个反面教材

// index.js

import _ from 'lodash';
import { each } from 'jquery';

console.log(
    _.join(['index', 'module', 'loaded!'], ' ')
);
复制代码
// main.js

import { each } from 'jquery';

let arr = ['zhangsan', 'lisi'];

each(arr, (idx, item) => {
    console.log(item);
})
复制代码
// webpack.config.js

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
	mode: 'development',
	entry: {
		index: './src/index.js',
		another: './src/main.js'
	},
	output: {
		filename: '[name].bundle.js',
		path: path.resolve(__dirname, 'dist')
	},
	plugins: [
		new HtmlWebpackPlugin()
	],
	module: {
		rules: [
			{
                test: /\.js$/,
				loaders: ['babel-loader'],
				include: path.join(__dirname, 'src')
			}
		]
	}
};
复制代码

两个入口,打包一把

又血崩了,发现index.bundle.js打包进了jquery,another.bundle.js也打包进了jquery,可以打开两个bundle.js进去查找下jquery看看是否打包进去。

那么理想状态下,应该是一份jquery,一份index,一份main,三份代码。用官方文档SplitChunksPlugin来试一把,index.js main.js webpack.config.js代码几乎不变。

// webpack.config.js中加上一项配置

optimization: {
	splitChunks: {
		chunks: 'all'
	}
}
复制代码

再打包一下

发现公共库被提取出来了,index和another两个文件只包含的业务代码

看一下chunk的里面的代码,发现只有verdors~other~index.bundle.js里面有require("jquery"),而vendors~index.bundle.js里面是直接用jquery的,并没有引入jquery源码

说明我们的效果已经达到了。

至此四个功能已经讲完了,自己写个demo来演练下吧,有问题可以加我微信demo0808

  • React的lazy懒加载
  • webpack的entry多入口
  • SplitChunksPlugin提取公共库代码
  • babel-plugin-import动态导入antd下的Button
关注下面的标签,发现更多相似文章
评论