前端性能优化—js代码打包
栏目: JavaScript · 发布时间: 6年前
内容简介:现在的 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
):
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 都可以删掉,此时打包结果如下:
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" ] } 复制代码
表示,除了 []
中的文件(类型),其他文件都是无副作用的,可以放心删掉。此时打包结果:
可以看到,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: ... 复制代码
动态导入
使用 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)}/> 复制代码
此时打包结果:
能看到, <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(); } 复制代码
如果有同学对Network面板不是很熟悉,可以看一下 Chrome DevTools — Network 。
提取复用的业务代码
第三方库代码已经单独提取出来了,但是业务代码中也会有一些复用的代码,典型的比如一些工具函数库 utils.js
。现在, About 组件
和 Docs 组件
都引用了 utils.js
,webpack 只打包了一份 utils.js
在 main.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> ); } } 复制代码
此时打包结果:
能够看到,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: ... 复制代码
再打包看结果:
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; 复制代码
打包结果:
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 页面的按需加载,达到了理想的加载效果。
缓存
项目打包后,资源部署在服务器端,客户端需要向服务器请求下载这些资源,用户才能看到内容。使用缓存,客户端可以大大减少不必要的请求和时间耽搁,只有当资源有更新时,再去下载。区分一个文件是否有更新,使用 文件名 + 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 不变。 打包前:
// About.scss .page-about { padding-left: 30px; color: #545880; // 修改字体颜色 } 复制代码
修改后:
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代码打包》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:- 前端的打包工具
- 如何优雅地打包前端代码
- 【前端打包部署】谈一谈我在SPA项目打包=>部署的处理
- 前端战五渣学前端——初探Parcel急速打包
- 前端项目之vue分环境打包
- 前端高级进阶:如何更好地优化打包资源
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。