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

栏目: JavaScript · 发布时间: 7年前

内容简介:现在的 web 应用,内容一般都很丰富,站点需要加载的资源也特别多,尤其要加载很多 js 文件。js 文件从服务端获取,体积大小决定了传输的快慢;浏览器端拿到 js 文件之后,还需要经过解压缩、解析、编译、执行操作,所以,本文从Tree Shaking 简单理解就是:打包时把一些没有用到的代码删除掉,保证打包后的代码体积最小化。其详细的介绍可以参考

现在的 web 应用,内容一般都很丰富,站点需要加载的资源也特别多,尤其要加载很多 js 文件。js 文件从服务端获取,体积大小决定了传输的快慢;浏览器端拿到 js 文件之后,还需要经过解压缩、解析、编译、执行操作,所以, 控制 js 代码的体积 以及 按需加载 对前端性能以及用户体验是十分的重要。

本文从 Tree Shaking代码分割 两部分介绍 js 打包优化,有兴趣的可以跟着一起实践。 clone 以下项目 github.com/jasonintju/… ,就是个简单的 React SPA,一看就懂。

Tree Shaking

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

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

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

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

修改 .babelrc

{
  "presets": [["env", { "modules": false }], "react", "stage-0"]
}
复制代码

修改 package.json

{
  "name": "optimizing-js",
  "version": "1.0.0",
  "sideEffects": false
}
复制代码

这样设置之后,表示所有的 module 都是无副作用的,没有使用到的 module 都可以删掉,此时打包结果如下:

前端性能优化—js代码打包
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 的值:

{
  "sideEffects": [
    "*.css", "*.scss", "*.sass"
  ]
}
复制代码

表示,除了 [] 中的文件(类型),其他文件都是无副作用的,可以放心删掉。此时打包结果:

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

可以看到,css 等样式文件现在如期打包进去了。如果有其他类型的文件有副作用,但是也希望打包进去,在 sideEffects: [] 中添加即可,可以是具体的某个文件或者某种文件类型。

关于为什么修改这两个地方就可以实现 Tree Shaking 的效果了,可以参考一下 developers.google.com/web/fundame… 或者其他文章,这里不做详细解释了。

代码分割

单页应用,如果所有的资源都打包在一个 js 里面,毫无疑问,体积会非常庞大,首屏加载会有很长时间白屏,用户体验极差。所以,要代码分割,分成一个一个小的 js,优化加载时间。

分离第三方库代码

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

module: {...},
optimization: {
  splitChunks: {
    cacheGroups: {
      venders: {
        test: /node_modules/,
        name: 'vendors',
        chunks: 'all'
      }
    }
  }
},
plugins: ...
复制代码
前端性能优化—js代码打包

动态导入

使用 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)}/>
复制代码

此时打包结果:

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

能看到, <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();
}
复制代码
前端性能优化—js代码打包

如果有同学对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>
    );
  }
}
复制代码

此时打包结果:

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

能够看到,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: ...
复制代码

再打包看结果:

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

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;
复制代码

打包结果:

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

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 页面的按需加载,达到了理想的加载效果。

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

缓存

项目打包后,资源部署在服务器端,客户端需要向服务器请求下载这些资源,用户才能看到内容。使用缓存,客户端可以大大减少不必要的请求和时间耽搁,只有当资源有更新时,再去下载。区分一个文件是否有更新,使用 文件名 + 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 不变。 打包前:

前端性能优化—js代码打包
// About.scss
.page-about {
  padding-left: 30px;
  color: #545880; // 修改字体颜色
}
复制代码

修改后:

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

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()]
复制代码

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


以上所述就是小编给大家介绍的《前端性能优化—js代码打包》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Web Design Handbook

Web Design Handbook

Baeck, Philippe de 编 / 2009-12 / $ 22.54

This non-technical book brings together contemporary web design's latest and most original creative examples in the areas of services, media, blogs, contacts, links and jobs. It also traces the latest......一起来看看 《Web Design Handbook》 这本书的介绍吧!

RGB转16进制工具
RGB转16进制工具

RGB HEX 互转工具

随机密码生成器
随机密码生成器

多种字符组合密码

HSV CMYK 转换工具
HSV CMYK 转换工具

HSV CMYK互换工具