全面了解 Code Spliting 和 Lazy Loading,前端性能优化实用干货

一、什么是 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

全面了解 Code Spliting 和 Lazy Loading,前端性能优化实用干货

(2)同步与异步差异

同步代码

如果我们使用的是同步的代码,在文件的顶部使用 import ... from '...'的语法导入第三方库,在使用 SplitChunksPlugin 配置后确实是可以实现代码分离。但是这样的分离后,在浏览器请求时会是同时请求,唯一提升性能的一点就是会命中浏览器的缓存。见下图:

全面了解 Code Spliting 和 Lazy Loading,前端性能优化实用干货

异步代码

对于使用 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,在浏览器空闲时提前加载了之后所需要的资源。

全面了解 Code Spliting 和 Lazy Loading,前端性能优化实用干货

(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…’, 然后再显示异步组件的内容。

全面了解 Code Spliting 和 Lazy Loading,前端性能优化实用干货

(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;

这里举例的只是组件的异步加载,该插件还能使用 SuspensePrefetching等等,如果你想深入了解,可以参考官方文档

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

六、写在文末

感谢你能够看到这里,最后记得给我个赞哦!很多东西都是我边学边做边记录的,可能会有些错误或不足的地方,欢迎大家在评论区指出来哈!

上一篇:POJ 3468 A Simple Problem with Integers(线段树+区间修改,区间查询)


下一篇:2019 GDUT 新生专题Ⅲ K题