前端性能优化—js代码打包

如今的 web 应用,内容通常都很丰富,站点须要加载的资源也特别多,尤为要加载不少 js 文件。js 文件从服务端获取,体积大小决定了传输的快慢;浏览器端拿到 js 文件以后,还须要通过解压缩、解析、编译、执行操做,因此,控制 js 代码的体积以及按需加载对前端性能以及用户体验是十分的重要。javascript

本文从 Tree Shaking代码分割 两部分介绍 js 打包优化,有兴趣的能够跟着一块儿实践。
clone 如下项目 https://github.com/jasonintju...,就是个简单的 React SPA,一看就懂。css

Tree Shaking

Tree Shaking 简单理解就是:打包时把一些没有用到的代码删除掉,保证打包后的代码体积最小化。其详细的介绍能够参考 Tree-Shaking性能优化实践 - 原理篇前端

项目 clone、安装依赖后,先 npm run build 打包初始代码,大小及分布以下(其中 src/utils/utils.js 这个文件打包后大小为11.72Kb):java

clipboard.png

src/containers/About/test.js只引用可是没有使用到,src/utils/utils.js 这个文件是个工具函数集,有不少不少函数,而咱们只用到了其中的一个。默认状况下,整个文件都被打包进 main.js 了,显然,这是很大的冗余,正好可使用 Tree Shaking 优化。node

修改 .babelrc

{
  "presets": [["env", { "modules": false }], "react", "stage-0"]
}

修改 package.json

{
  "name": "optimizing-js",
  "version": "1.0.0",
  "sideEffects": false
}

这样设置以后,表示全部的 module 都是无反作用的,没有使用到的 module 均可以删掉,此时打包结果以下:react

clipboard.png

import React from 'react';
// 只引入了 arraySum, utils.js 中的其余方法不会被打包
import { arraySum } from '@utils/utils';
import './test'; // 引用,“未使用”,不会被打包
import './About.scss'; // 引用,“未使用”,不会被打包

class About extends React.Component {
  render() {
    const sum = arraySum([12, 3]);
    return (
      <div className="page-about">
        <h1>About Page</h1>
        <div> 12 plus 3 equals {sum}</div>
      </div>
    );
  }
}
export default About;

如上面注释所说,Tree Shaking 认为这些是没有被使用的代码,因此能够删掉。但事实上咱们知道不是这样的,test.js 能够删掉,可是 css、scss 是有用的代码,咱们只需引入便可。所以,须要修改一下 sideEffects 的值:webpack

{
  "sideEffects": [
    "*.css", "*.scss", "*.sass"
  ]
}

表示,除了[]中的文件(类型),其余文件都是无反作用的,能够放心删掉。此时打包结果:git

clipboard.png

能够看到,css 等样式文件如今如期打包进去了。若是有其余类型的文件有反作用,可是也但愿打包进去,在 sideEffects: [] 中添加便可,能够是具体的某个文件或者某种文件类型。github

关于为何修改这两个地方就能够实现 Tree Shaking 的效果了,能够参考一下https://developers.google.com... 或者其余文章,这里不作详细解释了。web

代码分割

单页应用,若是全部的资源都打包在一个 js 里面,毫无疑问,体积会很是庞大,首屏加载会有很长时间白屏,用户体验极差。因此,要代码分割,分红一个一个小的 js,优化加载时间。

分离第三方库代码

第三方库代码单独提取出来,和业务代码分离,减小 js 文件体积。在 webpack.base.conf.js 中增长:

module: {...},
optimization: {
  splitChunks: {
    cacheGroups: {
      venders: {
        test: /node_modules/,
        name: 'vendors',
        chunks: 'all'
      }
    }
  }
},
plugins: ...

clipboard.png

动态导入

使用 ECMAScript 提案dynamic import 语法能够异步加载业务中的组件。使用方法以下:

// src/containers/App/App.js

// 注释掉此行代码
// import About from '@containers/About/About';

// 修改模块为动态导入形式
<Route path="/about" render={() => import(/* webpackChunkName: "about" */ '@containers/About/About').then(module => module.default)}/>

此时打包结果:

clipboard.png

能看到,<About> 组件已经被 webpack 单独打包出对应的 js 文件了。同时,结合 react-router,分离 <About> 组件的同时也作到了按需加载:当访问 About 页面时,about.js 才会被浏览器加载。

注意,咱们如今只是简单地使用了 dynamic import,不少边界状况没考虑进去,好比:加载进度、加载失败、超时等处理。能够开发一个高阶组件,把这些异常处理都包含进去。社区有个很棒的 react-loadable,大树底下好乘凉~

npm i react-loadable

// src/containers/App/App.js
import Loadable from 'react-loadable';

// 代码分割 & 异步加载
const LoadableAbout = Loadable({
  loader: () => import(/* webpackChunkName: "about" */ '@containers/About/About'),
  loading() {
    return <div>Loading...</div>;
  }
});

class App extends React.Component {
  render() {
    return (
      <BrowserRouter>
        <div>
          <Header />

          <Route exact path="/" component={Home} />
          <Route path="/docs" component={Docs} />
          <Route path="/about" component={LoadableAbout} />
        </div>
      </BrowserRouter>
    );
  }
}

react-loadable 还提供了 preload 功能。假若有统计数据显示,用户在进入首页以后大几率会进入 About 页面,那咱们就在首页加载完成的时候去加载 about.js,这样等用户跳到 About 页面的时候,js 资源都已经加载好了,用户体验会更好。

// src/containers/App/App.js
componentDidMount() {
  LoadableAbout.preload();
}

clipboard.png

若是有同窗对Network面板不是很熟悉,能够看一下 Chrome DevTools — Network

提取复用的业务代码

第三方库代码已经单独提取出来了,可是业务代码中也会有一些复用的代码,典型的好比一些工具函数库 utils.js。如今,About 组件Docs 组件都引用了 utils.js,webpack 只打包了一份 utils.jsmain.js 里面,main.js 在首页就被加载了,其余页面有使用到 utils.js 天然能够正常引用到,符合咱们的预期。可是目前咱们只是把 About 页面异步加载了,若是把 Docs 页面也异步加载了会怎么样呢?

// src/containers/App/App.js
// 注释掉此行代码
// import Docs from '@containers/Docs/Docs';

const LoadableDocs = Loadable({
  loader: () => import(/* webpackChunkName: "docs" */ '@containers/Docs/Docs'),
  loading() {
    return <div>Loading...</div>;
  }
});

class App extends React.Component {
  render() {
    return (
      <BrowserRouter>
        <div>
          <Header />

          <Route exact path="/" component={Home} />
          <Route path="/docs" component={LoadableDocs} />
          <Route path="/about" component={LoadableAbout} />
        </div>
      </BrowserRouter>
    );
  }
}

此时打包结果:

clipboard.png

可以看到,about.js 和 docs.js 里面都打包了 utils.js,重复了!
webpack.base.conf.js 中增长:

module: {...},
optimization: {
  splitChunks: {
    cacheGroups: {
      venders: {
        test: /node_modules/,
        name: 'vendors',
        chunks: 'all'
      },
      default: {
        minSize: 0,
        minChunks: 2,
        reuseExistingChunk: true,
        name: 'utils'
      }
    }
  }
},
plugins: ...

再打包看结果:

clipboard.png

utils.js 也被单独打包出来了,达到了预期。

分离非首页使用且复用程度小的第三方库

假如,如今 Docs.js 引用了 lodash 这个三方库:

import React from 'react';
import _ from 'lodash';
import { arraySum } from '@utils/utils';
import './Docs.scss';

class Docs extends React.Component {
  render() {
    const sum = arraySum([1, 3]);
    const b = _.sum([1, 3]);
    return (
      <div className="page-docs">
        <h1>Docs Page</h1>
        <div> 1 plus 3 equals {sum}</div>
        <br />
        <div>use _.sum, 1 plus 3 equals {b} too.</div>
      </div>
    );
  }
}
export default Docs;

打包结果:

clipboard.png

lodash.js 只在 Docs 页面使用,并且可能 Docs 页面访问量不多,把 lodash.js 打包在首页就会加载的 venders.js 里面,实在不是明智之举。

修改 webpack.base.conf.js

...
venders: {
  test: /node_modules\/(?!(lodash)\/)/, // 去除 lodash,剩余的第三方库打成一个包,命名为 vendors-common
  name: 'vendors-common',
  chunks: 'all'
},
lodash: {
  test: /node_modules\/lodash\//, // lodash 库单独打包,并命名为 vender-lodash
  name: 'vender-lodash'
},
default: {
  minSize: 0,
  minChunks: 2,
  reuseExistingChunk: true,
  name: 'utils'
}
...

此时把 lodash 单独打成了一个包,且配合 Docs 页面的按需加载,达到了理想的加载效果。

clipboard.png

缓存

项目打包后,资源部署在服务器端,客户端须要向服务器请求下载这些资源,用户才能看到内容。使用缓存,客户端能够大大减小没必要要的请求和时间耽搁,只有当资源有更新时,再去下载。区分一个文件是否有更新,使用 文件名 + hash 能够达到目的。本案例中,已经使用了 '[name].[contenthash:8].js'

然而,在打包的时候,webpack的运行时代码有时候会致使某些状况出现,如:什么内容都没改,两次 build 代码的 hash 不同;或者是,修改了 a 文件的代码,却致使了某些未修改代码文件的 hash 也发生了变化。This is caused by the injection of the runtime and manifest which changes every build.

注意:使用的 webpack 版本不一样,可能会致使打包出的结果不同。较新的版本或许没有这种 hash 问题,但为了安全起见,仍是建议按照下面的步骤处理一下。

分离 webpack runtimeChunk code

// webpack.base.conf.js
optimization: {
  runtimeChunk: {
    name: 'manifest'
  },
  splitChunks: {...}
}

此时,能达到:修改某个文件,只有这个文件和 manifest.js 文件的 hash 会发生变化,其余文件的 hash 不变。
打包前:

clipboard.png

// About.scss
.page-about {
  padding-left: 30px;
  color: #545880; // 修改字体颜色
}

修改后:

clipboard.png

HashedModuleIdsPlugin

增长、删除一些模块,可能会致使不相关文件的 hash 发生变化,这是由于 webpack 打包时,按照导入模块的顺序,module.id 自增,会致使某些模块的 module.id 发生变化,进而致使文件的 hash 变化。

解决方式: 使用 webpack 内置的 HashedModuleIdsPlugin,该插件基于导入模块的相对路径生成相应的 module.id,这样若是内容没有变化加上 module.id 也没变化,则生成的 hash 也就不会变化了。

// webpack.prod.conf.js
const webpack = require('webpack');
...
plugins: [new webpack.HashedModuleIdsPlugin(), new BundleAnalyzerPlugin()]

完整的优化代码见 https://github.com/jasonintju...


有用的文章:
webpack分离第三方库及公用文件
https://developers.google.com...

相关文章
相关标签/搜索