一、什么是 Code Spliting 和 Lazy Loading ?
简单的来讲,就是我们将代码进行 Code Spliting
(代码分离),分离成很多块(chunk
) ,然后可以按需要加载不同的代码,也就是 Lazy Loading
(懒加载)。这样的话,可以极大的减少我们的初始化时的请求次数,而且还可以命中浏览器的缓存,避免多次请求相同代码,来提升网页加载的速度。
二、了解 import() 函数
在正式进入学习代码分离和懒加载之前,我们需要提前了解 import()
函数,因为之后很多地方都要用到它。
它是 ES2020 提案的内容,目前处于TC39流程的第 4 阶段。它类似于 commonJS
中的 require
, 能够动态加载模块。
基础使用
import()
返回一个 Promise
对象,可以使用 .then()
获取返回的对象,当做它的参数,该参数可以使用解构赋值的语法获取。
document.addEventListener('click', () => {
import('lodash').then(({default: _ }) => {
console.log(_.join(['Axton', 'Tang'], ' '))
})
})
注意:浏览器不支持直接使用 import()
函数,可以在 webpack
中使用 @babel/polyfill
进行语法转换。
好了,到这里大家对 import()
应该有个大概了解了,下面开始进入正题。
三、Webpack 中的配置与实现
1、 JS 代码分离的三种方式
很多时候我们会使用到像 lodash
这样的第三方库,如果将它打包到我们的主代码中,会使打包后文件过大。为了解决此问题,下面开始介绍不同的配置方式。
(1)手动实现代码分离
主文件为 index.js
, 在 lodash.js
文件中导入 lodash
库,在 webpack
配置文件中配置多个入口。
// index.js
console.log(_.join(['Axton', 'Tang'], '-'))
// lodash.js
import _ from 'lodash'
window._ = _ // 将 '_' 绑定到全局使用
// webpack.config.js
module.exports = {
entry: {
lodash: './src/lodash.js',
main: './src/index.js'
}
//...
}
此种方法存在的问题是,如果多个模块之间存在公共的一些部分,将会重复引用到 bundle
中。所以,不推荐使用。
(2)使用 SplitChunksPlugin 插件
快速使用
在配置文件中添加以下代码即可分离同步及异步代码。
// webpack.config.js
module.exports = {
// ...
optimization: {
splitChunks: {
chunks: 'all'
}
}
// ...
}
这样将会新生成一个以 vendor
开头的 chunk
, 能够自动实现主代码和导入的第三方库的分离,而且还能去除掉重复依赖的模块。
了解配置
在进行上面简单的配置后,很快就能够实现自动分离。但是, webpack
在背后帮我们添加了很多默认的配置项。
module.exports = {
//...
optimization: {
splitChunks: {
chunks: 'async', //三选一:"initial" 初始化,"all"(同步异步都有效),"async"(只对异步有效)
minSize: 20000, // 形成一个新代码块最小的体积,只有 >= minSize 的bundle会被拆分出来
minRemainingSize: 0, // 确保拆分后剩余的最小块大小超过此设置,来避免大小为零的模块
maxSize: 0, //拆分之前最大的数值,默认为0,即不做限制
minChunks: 1, //引入次数,如果为2 那么一个资源最少被引用两次才可以被拆分出来
maxAsyncRequests: 30, // 按需加载的最大并行请求数
maxInitialRequests: 30, // 一个入口最大并行请求数
automaticNameDelimiter: '~', // 文件名的连接符
enforceSizeThreshold: 50000, //强制执行拆分的大小阈值
cacheGroups: { // 缓存组可以继承或覆盖以上任何选项
defaultVendors: {
test: /[\\/]node_modules[\\/]/, // 正则表达式验证,如果符合就提取 chunk
priority: -10, // 缓存组优先级,即权重
reuseExistingChunk: true // 可设置是否重用已用chunk,不再创建新的chunk
},
default: {
minChunks: 2,
priority: -20,
reuseExistingChunk: true
}
}
}
}
};
大部分时候使用其默认配置就可以了,如果有更高的要求需要配置,可以参考 此文档
(3)使用 import() 函数动态导入
对于使用 import()
的异步代码,我们并不需要进行 optimization.splitChunks
的配置,打包后会自动分离到一个新的 chunk
中。
import('lodash').then(({default: _ }) => {
console.log(_.join(['Axton', 'Tang'], ' '))
})
2、懒加载与缓存
(1)简单使用
既然能够使用 import()
动态导入,那么在外层加上一些触发的条件,那就是懒加载了。
btn.addEventListener('click', () => {
import('lodash').then(({default: _ }) => {
console.log(_.join(['Axton', 'Tang'], ' '))
})
})
当我们点击按钮后,才会请求这个新的 chunk
(2)同步与异步差异
同步代码
如果我们使用的是同步的代码,在文件的顶部使用 import ... from '...'
的语法导入第三方库,在使用 SplitChunksPlugin
配置后确实是可以实现代码分离。但是这样的分离后,在浏览器请求时会是同时请求,唯一提升性能的一点就是会命中浏览器的缓存。见下图:
异步代码
对于使用 import()
函数的异步代码,只有当我们使用时才会发出请求。有时,我们可能永远都不会用到,那么能极大的提高性能。在之前配置 splitChunks.chunks
时,默认的值为 async
,那是因为官方并不推荐我们使用同步代码进行如此的分离。因为同步代码仍然是同时请求,使用缓存也只对之后再次访问才有效,对性能的提升是很低的。
所以,之后我们引入第三方库的时候都要尽量使用异步的方式。
3、Preloading 和 Prefetching
讲到这里,可能大家会有疑问,对于一些我们暂时使用不到的 chunk
初始时都不进行加载,在用到时再加载,那这样会不会导致之后执行某些操作会加载很慢呢?
确实可能会这样,因此我们现在来讲预加载。
- prefetch(预获取):加载将来某些导航下可能需要的资源
-
preload(预加载):加载当前导航下可能需要资源, 会与主
chunk
同步加载,不推荐使用。
使用 prefetch
会在我们主要代码加载完毕后,浏览器空闲的时间里,提前加载代码,使用方式是在 import()
中添加一段 “魔法注释”。
btn.addEventListener('click', () => {
import(/* webpackPrefetch: true */ 'lodash').then(({default: _ }) => {
console.log(_.join(['Axton', 'Tang'], ' '))
})
})
4、对 CSS 代码进行分离
首先安装 mini-css-extract-plugin
:
npm install --save-dev mini-css-extract-plugin
之后进行如下配置即可生成一个 css
文件的 chunk
style.css
body {
background: blue;
}
component.js
import './style.css';
webpack.config.js
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = {
plugins: [new MiniCssExtractPlugin({
filename: '[name].css', // 打包后入口文件的名字
chunkFilename: '[name].chunk.css' // 打包后非入口文件的名字
})],
module: {
rules: [
{
test: /\.css$/i,
use: [MiniCssExtractPlugin.loader, 'css-loader'],
},
],
},
};
四、Vue 中的配置与应用
1、异步组件
异步组件为动态导入的组件,仅当使用到它时才加载。
(1)在Vue 3 中的新用法
对于 Vue3
中的一些语法新特性,大家可以看一下我的这篇博客。
在 vue3
中新增了一个方法:defineAsyncComponent
,用来显式地定义异步组件
<template>
<div>
<button @click="handleClick">按钮</button> <!-- 按需要来控制组件的显示 -->
<AsyncComponent v-if="show" />
</div>
</template>
<script>
import { defineAsyncComponent, ref } from "vue";
export default {
name: 'Home',
components: {
AsyncComponent: defineAsyncComponent(() =>
import("../../components/AsyncComponent") // 动态导入组件
),
},
setup() {
let show = ref(false)
const handleClick = () => {
show.value = true
}
return {
show,
handleClick
}
}
};
</script>
上面代码中我们使用 show
变量来控制这个异步组件的显示。然而,在下图中我们查看请求时发现 0.js
已经提前加载完毕了。其实,这就是我们之前 webpack
的配置时所说的 prefetch
,在浏览器空闲时提前加载了之后所需要的资源。
(2)在 Vue 2.x 中的用法
export default {
name: 'Home',
components: {
AsyncComponent: () => import("../../components/AsyncComponent") // 动态导入组件
},
};
2、路由的懒加载
对于暂未跳转到的路由不进行相关资源加载,用到时再加载。但是在实际应用中,还是会像上面的异步组件一样,在浏览器空闲时进行 prefetch
。
import { createRouter, createWebHistory } from 'vue-router'
import Home from '../pages/Home/Home.vue'
const routerHistory = createWebHistory()
export default createRouter({
history: routerHistory,
routes: [{
path: '/',
component: Home
}, {
path: '/demo',
component: () => import('../pages/Demo/Demo.vue') // 路由懒加载
}]
})
五、React 中的配置与应用
1、异步组件
(1)使用 React.lazy 和 Suspense
React.lazy
接受一个函数,这个函数需要动态调用 import()
。React.lazy
加载的组件只能在 <Suspense>
组件下使用,代码如下:
import React, { Suspense, Component } from 'react'
const LazyComponent = React.lazy(() => import('./components/LazyComponent'));
class Home extends Component {
constructor(props) {
super(props);
this.state = {
show: false
};
this.handleClick = this.handleClick.bind(this);
}
render() {
return (
<div>
<button onClick={this.handleClick}>按钮</button>
{
this.state.show &&
(<Suspense fallback={<div>Loading...</div>}>
<LazyComponent />
</Suspense>)
}
</div>
)
}
handleClick() {
this.setState({
show: true
})
}
}
export default Home;
结果如下图所示,我们可以看到点击按钮后 2.chunk.js
才加载出来,页面上先会显示 ‘Loding…’, 然后再显示异步组件的内容。
(2)使用 @loadable/components
示例:
import React, { Component } from 'react'
import loadable from '@loadable/component'
const LazyComponent = loadable(() => import('./components/LazyComponent'))
class Home extends Component {
constructor(props) {
super(props);
this.state = {
show: false
};
this.handleClick = this.handleClick.bind(this);
}
render() {
return (
<div>
<button onClick={this.handleClick}>按钮</button>
{
this.state.show && <LazyComponent />
}
</div>
)
}
handleClick() {
this.setState({
show: true
})
}
}
export default Home;
这里举例的只是组件的异步加载,该插件还能使用 Suspense
、 Prefetching
等等,如果你想深入了解,可以参考官方文档。
使用 React.lazy 和 @loadable/components 的区别:
React.lazy
是由React
官方维护,推荐的代码拆分的解决方案。React.lazy
只能与Suspense
一起使用,而且不支持服务端渲染。@loadable/component
支持服务端渲染。
说明:react-loadable
也可以进行 React
的代码拆分,但是由于它已经没有被维护,并且与Webpack v4 +和Babel v7 +不兼容,所以,还是推荐使用以上两种解决方案。
2、路由的懒加载
import React, { Suspense, lazy } from 'react';
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
const Home = lazy(() => import('./pages/home/index'));
const List = lazy(() => import('./pages/List/index'));
function App() {
return (
<div className="App">
<Router>
<Suspense fallback={<div>Loading...</div>}>
<Switch>
<Route exact path="/" component={Home}/>
<Route path="/list" component={List}/>
</Switch>
</Suspense>
</Router>
</div>
);
}
export default App;
六、写在文末
感谢你能够看到这里,最后记得给我个赞哦!很多东西都是我边学边做边记录的,可能会有些错误或不足的地方,欢迎大家在评论区指出来哈!