对Webpack 应用的研究-----------------引用
对大多数 Web 应用来说,页面性能直接影响着流量。这是一个经常为我们所忽视的事实。用户长时间的等待流失的不仅仅是跳出率、转化率,还有对产品的耐心和信赖。很多时候我们没有意识到性能问题,那是因为平常开发使用的都是高效的设备和网络。而到了真实世界中却会发现,实际用户的网络环境会更加复杂,而如果使用的是移动设备的话,有限的计算能力也会拖慢代码的解析执行,这些都会影响页面的渲染效率。
Web 应用的加载速度很大程度上取决于资源的大小,下面是 Youtube 桌面端页面通过 PageSpeed Insights 检测得到的数据,整个页面渲染加载了 2861 KB 的资源,其中 JavaScript 占了大头。
下面我总结了一些借助 Webapck 进行构建的工程可以采用的一些优化输出资源体积的方法,使打包出的 JavaScript、CSS 文件更小,页面加载更快。
代码分片
首先让我们从代码分片(code splitting)说起。代码分片就是通过把原本的代码进行“提取”和“分离”使客户端尽可能地只加载当前需要的资源。
曾经遇到过一个这样一个工程,它有十几个页面,每个页面都引用了其所使用的框架以及 UI 库,导致产出的资源体积非常大,有时在打包过程中直接就内存溢出崩掉了。后来通过代码分片把公共模块提取 到了单独的文件中,再让各个页面分别引用它,整个打包结果的体积只有原先的几分之一,并且也不再有内存溢出的问题了。
上面说的“提取”指的是找到代码中重复的部分或者是不经常变动的部分,并将其作为一个独立的资源打包出来。在 Webpack4 之前通常使用 CommonsChunkPlugin,但它在设计上存在一些问题,并且在某 些场景下难以使用,在 Webpack4 时就被官方替换为了 SplitChunksPlugin。
对 CommonsChunkPlugin 熟悉的人应该清楚,使用这个插件时要通过各种配置项对指定入口的指定模块进行提取,让人感觉像是命令式的;相比之下 SplitChunksPlugin 则更像是声明式的——由使用者来定义提取规则,比如新的 Chunk 必须可以被共享以及体积要大于 30KB 等等,当模块满足了这些规则就会被提取出来。这样灵活性更强,对使用者也更加友好。
下面是一个使用 SplitChunksPlugin 的例子:
module.exports = {
entry: {
pageA: './pageA.js',
pageB: './pageB.js',
},
output: {
filename: [name].js,
},
mode: 'development',
optimization: {
splitChunks: {
chunks: 'all',
},
},
};
在该配置下,如果 pageA 和 pageB 包含了一些体积比较大的公共模块,那么它们就会自动地被提取出来,结果如下图。
说完了“提取”再说一下“分离”。它是指将部分代码延迟加载或者说动态加载,在 Webpack 中通过import() 语法来实现。请看下面这个例子:
// util.js
export function add(a, b) {
return a + b;
}
// index.js
import('./util').then(({ add }) => {
console.log(add(2, 3));
});
使用import()加载的模块及其依赖模块会构建出一个 async chunk,并在页面上延迟加载。比如上面的例子中的 util.js 将不会被打包到 index.js 的 bundle 中,而是在浏览器加载完 index.js 后再去请求 util.js,等 util.js 加载完成后后再去执行回调函数里面的逻辑。
这种方法适合于处理第三方库以及用户不会立即使用的功能,或者配合 SPA 路由,将页面级别的代码全部使用动态加载。
比如在 Vue 中,我们可以这样实现:
const Home = () => import('./Home.vue');
const router = new VueRouter({
routes: [
{ path: '/', component: Home }
]
});
类似的 React 的例子,结合 React.lazy 与 Suspense 也可以有相同的效果:
const Home = lazy(() => import('./routes/Home'));
const App = () => (
<Router>
<Suspense fallback={<div>Loading...</div>}>
<Switch>
<Route exact path="/" component={Home}/>
</Switch>
</Suspense>
</Router>
);
排除非必要资源
有些时候在加载了一个库、框架或者工具之后,也会连带地加载一些不必要的资源,使打包结果体积无故增大了许多。
这个问题最常见的就是对 Moment.js 的使用。Moment.js 是一个用于处理时间和日期的库,它支持非常多的语言。这是一个非常方便的特性,比如我们可以用中文显示一个日期离现在有多久,会得到“一小时前”或者“两天前”等等。但同时这个特性也有一个问题,即默认情况下它会加载进所有语言包。比如下面这个例子。
import moment from 'moment';
console.log(moment());
当加载了 moment 模块之后,我们在打包结果中会看到非常多类似./node_modules/moment/locale/zh-cn.js的语言包,所有这些由引入 Moment.js 带来的模块最后产生的 bundle.js 有 600KB。
为了解决这个问题,我们可以借助 IgnorePlugin 将语言包模块进行忽略:
new Webpack.IgnorePlugin(/^\.\/locale$/, /moment$/)
重新打包后产出的 bundle.js 仅有 233KB
如果需要保留一些特定的语言,只要直接在代码中加载特定的语言包模块就可以了。请看这个例子。
// index.js
import moment from 'moment';
import 'moment/locale/zh-cn';
上面由于直接采用了模块路径的形式来加载,它并不会被我们配置的 IgnorePlugin 匹配到,因此依旧会打包到最后的 bundle 中。
减小 CSS 体积
相比于 JavaScript 和图片来说,CSS 的体积通常没有那么大,但对整个页面的渲染性能来说 CSS 仍然是十分重要的一环。因为页面的初始渲染一定是要等 CSS 加载完成后再进行页面内容排布的,CSS 的体积将直接影响到用户从开始请求页面到看到有意义内容的时间,这个时间是评估页面性能的一项关键指标。
减小 CSS 体积要做的第一件事是压缩代码,下面是一个提取 CSS 代码到文件并进行压缩的示例。
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
module.exports = {
optimization: {
minimizer: [new OptimizeCSSAssetsPlugin({})],
},
plugins: [
new MiniCssExtractPlugin({
filename: '[name].css',
chunkFilename: '[id].css',
})
],
module: {
rules: [
{
test: /\.css$/,
use: [MiniCssExtractPlugin.loader, 'css-loader'],
}
],
},
};
导致 CSS 文件体积较大的情况通常是由于代码中包含了过多没用的样式。通过 Chrome dev tools 可以获取到当前页面中所使用到的 CSS 的占比,帮助检查出冗余的样式代码。
另一个容易使 CSS 文件体积过大的是 url-loader。如果在 Webpack 配置中使用了 url-loader 的话要注意 CSS 的内容中是不是包含了过多图片的 base64 URI。url-loader 的 limit 如果设置的比较大,同时页面又有很多小的图片,并且由于 base64 URI 的 gzip 效果很差,很容易就会使 CSS 的体积变得很大。
下面的示例将 url-loader 的 limit 设为 2 KB,具体的数值设置要根据项目实际情况。
rules: [
{
test: /\.(png|jpg|gif)$/i,
use: [
{
loader: 'url-loader',
options: {
limit: 2048,
},
}
],
}
]
使用 Brotli 进行资源压缩
Brotli 是由 Google 开发的无损压缩算法,可以在几乎相同的速度下比 gzip 得到更好的压缩效果,并且它已经被绝大多数现代浏览器所支持:
有人通过大量网络上的资源对 Brotli 和 gzip 进行了一个对比:
对于 JavaScript 文件,Brotli 产出的压缩结果比 Gzip 小了 14%;
HTML 文件缩小了 21%;
CSS 文件缩小了 17%。
有很多工具可以让我们在构建流程中使用 Brotli 进行资源压缩,对于 Webpack 工程的话可以直接使用brotli-webpack-plugin。请看下面的例子。
var BrotliPlugin = require('brotli-webpack-plugin');
module.exports = {
plugins: [
new BrotliPlugin({
asset: '[path].br',
test: /\.(js|css|svg)$/
})
],
};
通过上面的配置,Webpack 在打包后会在原有资源的基础上生成一个.br文件,也就是经过 Brotli 压缩后的版本。我们可以将它与原有的资源文件一同上传到 CDN,这样如果浏览器不支持 Brotli,也可以使其回退来使用 gzip。
Brotli 生效的话,返回头中 content-encoding 的值应该为 br。
资源打包分析和监控
最后也是最重要的一点,是对项目资源进行持续的监控和分析。下面介绍几个比较常用的工具。
webpack-bundle-analyzer
webpack-bundle-analyzer 借助可视化的方式直观地展示输出资源的构成,比如下面的例子。
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
module.exports = {
plugins: [
new BundleAnalyzerPlugin(),
],
};
以上面提到过的 moment.js 为例,会得到下面这样一个分析图:
从图中可以发现 moment.js 中 locale 文件过多的问题。实际工程的情况会比这个更复杂,但仍然可以帮助我们排查冗余模块的存在。
size-plugin
size-plugin 是一个 Webpack 插件,可以在每次执行打包命令后打印出本次构建的资源体积并和上次构建结果进行对比。
const SizePlugin = require('size-plugin');
module.exports = {
plugins: [new SizePlugin()],
};
Import Cost
Import Cost 是一个 VSCode 的一个扩展,可以在模块加载语句旁边展示出所加载模块的大小。
对Webpack 应用的研究-----------------引用的更多相关文章
- webpack打包绝对路径引用资源和element ui字体图标不显示的解决办法
webpack打包绝对路径引用资源解决办法: 打开webpack.prod.conf.js 找到output:增加 publicPath: './', 即可,如图 element ui字体图标不显 ...
- [Vuejs] webpack+vue-cli打包如何引用相对路径
默认情况下通过webpack+vuec-li打包的css.js等资源,路径都是绝对的,即static在根目录下,假如部署到带有文件夹目录的项目中,资源路径就会出错,如何解决. 1.修改资源引用相对路径 ...
- webpack打包jquery并引用
一,引入webpack插件 //打包第三方 const CommonsChunkPlugin = require("webpack/lib/optimize/CommonsChunkPlug ...
- vue+webpack静态资源路径引用
处理静态资产 你可能已经注意到,在项目结构中我们有两个静态资产目录:src/assets和static/.他们之间有什么区别? 要回答这个问题,我们首先需要了解Webpack如何处理静态资产.在*.v ...
- 对vue-router的研究--------------引用
pushState/replaceState/popstate 解析 HTML5提供了对history栈中内容的操作.通过history.pushState/replaceState实现添加地址到hi ...
- 对webpack的初步研究8
模块 编辑文档 在模块化编程中,开发人员将程序分解为称为模块的离散功能块. 每个模块的表面积小于完整程序,使验证,调试和测试变得微不足道.编写良好的模块提供了可靠的抽象和封装边界,因此每个模块在整个应 ...
- webpack 3.X研究
目前webpack已经到了3.X版本,为了方便通过最新版本打包整个前端应用,现在开始对webpack操作进行简单介绍. 一.webpack安装 在安装webpack之前需要注意,webpack是通过n ...
- 对JS继承的研究--------------引用
问:类继承和原型继承不是同一回事儿吗,只是风格选择而已? 答:不是! 类继承和原型继承不论从本质上还是从语法上来说,都是两个截然不同的概念. 二者之间有着区分彼此的本质性特征.要完全看懂本文,你必须牢 ...
- 对数据劫持 OR 数据代理 的研究------------引用
数据劫持,也叫数据代理. 所谓数据劫持,指的是在访问或者修改对象的某个属性时,通过一段代码拦截这个行为,进行额外的操作或者修改返回结果.比较典型的是 Object.defineProperty() 和 ...
随机推荐
- Linux就该这么学——重要的环境变量
Linux命令执行过程 1.判断用户是否以绝对路径或相对路径的方式输入命令(如 /bin/ls) ,如果是的话则直接执行 2.Linux系统检查用户输入的命令是否为”别名命令”. 即用一个自定义的命令 ...
- linux的安装和配置
转载:https://www.cnblogs.com/hhaahh/p/10404093.html 1.VMware简介 此软件是一个虚拟的pc机软件,可以在现有操作系统中虚拟出一个新的硬件环境,以此 ...
- maraidb忘记数据密码
一.概述 服务器上安装了maraidb 数据库,但是很久未使用过它,需要使用时,忘记了密码, 此时可以给它重新设置密码. 二.操作 修改密码 修改 /etc/my.cnf,修改下图红色区域位置,修改成 ...
- 这周末又参加班里同学生日party,同学父母包场2小时花费大约1000美金左右。
今天班上Claire的生日,邀请了几个小朋友去pump it up.特别特别开心,因为她父母选的时间特别好晚上6-8点小孩子玩疯了以后吃的特别多.
- 客户端注册Cannot execute request on any known server解决
在对eureka注册中心服务端添加安全验证后,新版本springcloud出现一个问题就是,在客户端注册到服务中心时报了一个错:Cannot execute request on any known ...
- java代码实现图片内容转文字
前言 现在的手机已经可以实现拍照转文字了.作为一名程序员,得使用java代码实现这一功能,虽然可能没啥用!!! pom.xml 添加依赖 <dependency> <groupId& ...
- java web浏览器访问工程前面添加一级目录学习笔记
有的时候因为nginx和f5策略问题需要在访问系统的时候前面添加一个前缀以区分不通地方需要用同一个工程的情况: 举个例子:当访问localhost:8080/webDemo/demo/shafei.j ...
- 云端js动态效果
效果图: <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3 ...
- Docker 容器数据卷(Data Volume)与数据管理
卷(Volume)是容器中的一个数据挂载点,卷可以绕过联合文件系统,从而为Docker 提供持久数据,所提供的数据还可以在宿主机-容器或多个容器之间共享.通过卷,我们可以可以使修改数据直接生效,而不必 ...
- SSM框架中的Mapper.xml文件中的增、删、改、查等操作
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "- ...