现在的 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.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>);}
}
复制代码

此时打包结果:

能够看到,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-commonname: 'vendors-common',chunks: 'all'
},
lodash: {test: /node_modules\/lodash\//, // lodash 库单独打包,并命名为 vender-lodashname: '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/…


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

前端性能优化—js代码打包相关推荐

  1. 前端性能优化常用代码

    前端性能优化常用代码 为什么要做性能优化?性能优化到底有多重要? 网站的性能优化对于用户的留存率.转化率有很大的影响,所以对于前端开发来说性能优化能力也是重要的考察点. 性能优化的点非常的多,有的小伙 ...

  2. ajax预加载html seo,前端性能优化 — JS预加载和懒加载

    JS预加载 需求:有时我们需要实现例如快速快速切换页面.图片之类的功能时,能尽快的加载出我们所需的图片会极大提升用户体验,这时用预加载将图片先缓存到浏览器,用户使用需显示图片时无疑会顺畅很多. 核心: ...

  3. 前端服务器获取js文件偶尔慢_我所认识的前端性能优化

    现象: 用户体验差 网页太卡打不开(卡.慢) 服务器带宽流量(成本) 服务器压力 从哪处理:各处的缓存 地址缓存 减少DNS的解析请求.预解析DNS(不是"解析DNS") TCP缓 ...

  4. 前端性能优化——从 10 多秒到 1.05 秒

    https://lishaoy.net 关于 性能优化 是个大的面,这篇文章主要涉及到 前端 的几个点,如 前端性能优化 的流程.常见技术手段.工具等. 提及 前端性能优化 ,大家应该都会想到 雅虎军 ...

  5. 前端性能优化 -- 从 10 多秒到 1.05 秒

    关于 性能优化 是个大的面,这篇文章主要涉及到 前端 的几个点,如 前端性能优化 的流程.常见技术手段.工具等. 提及 前端性能优化 ,大家应该都会想到 雅虎军规,本文会结合 雅虎军规 融入自己的了解 ...

  6. 关于前端性能优化问题,认识网页加载过程和防抖节流

    前端性能优化-网页加载过程.性能优化方法.防抖和节流 一.网页加载过程 1.加载资源的形式 2.加载资源的过程 3.渲染页面的过程 4.关于window.onload 和 DOMContentLoad ...

  7. 【Day11】平时在项目开发中都做过哪些前端性能优化

    平时在项目开发中都做过哪些前端性能优化 一.体验优化 二.提升页面性能 三.首页加载优化(减少白屏时间) 一.体验优化 从用户角度而言,优化能够让页面加载得更快.对用户的操作响应得更及时,能够给用户提 ...

  8. dll文件懒加载_前端性能优化

    # 前端性能优化 写在最前面:下面都是我对webpack的一些性能优化,想系统的学习性能优化方面的知识 推荐大家看看这本书 很系统 感觉面试也能如鱼得水 ## 构建优化 ### webpack优化 ( ...

  9. 前端性能优化的七种方法

    前端性能优化主要有七种方法,包括减少请求数量.减少资源大小.优化网络连接.优化资源加载.减少重绘回流.使用性能更好的API和webpack优化 1.减少请求数量 1.1 图片处理 1.1.1 雪碧图 ...

最新文章

  1. react 自定义 TabBar 组件
  2. Android 自定义动画 LoadingView
  3. Spark2.4.0 SparkEnv 源码分析
  4. 你每隔多久使用计算机上网查找资料英文,牛津英语8B Unit3导学案
  5. 使用GNOME桌面工具管理Linux(4)–服务的使用
  6. python里的文件I/O
  7. 数据库清空表中的数据
  8. git 使用_Git-介绍与使用
  9. Freemarker简单封装
  10. 生成式对抗网络GAN生成手写数字
  11. GIGO1.1数据清洗利器,文献计量同义词合并
  12. 向奇汉:服务企业互联网化 打造社会化商业平台
  13. 是非人生 — 一个菜鸟程序员的5年职场路 第30节
  14. 解决win10以太网没有有效的ip配置
  15. 为什么80%的企业会选择云产品?
  16. 卡马克揭开VR延迟背后的真相
  17. delphi 高仿微信气泡聊天窗口
  18. 【光学设计基础】--02球差
  19. python统计次数正则_Python提取信息必学基础——正则表达式
  20. 2018年20个主要的大数据认证

热门文章

  1. pythonurllib模块-Python中的urllib模块使用详解
  2. python语言程序设计基础网课-Python语言程序设计基础答案
  3. 初学者学python好还是c-学Python还是学C?
  4. python 命令行参数-Python 命令行参数解析
  5. python编程入门指南 代码库在哪下-致Python初学者 Anaconda入门使用指南完整版
  6. python语言怎么输入-python/基础输出输入用法
  7. python处理多个excel表-使用python把Excel表格由一个分隔成多个
  8. python软件安装-学python安装的软件总结
  9. UVa12467 Secret Word(kmp)
  10. RabbitMQ中的消息不可达returnlistener和mandatory的使用