对大多数 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 应用的研究-----------------引用的更多相关文章

  1. webpack打包绝对路径引用资源和element ui字体图标不显示的解决办法

    webpack打包绝对路径引用资源解决办法: 打开webpack.prod.conf.js 找到output:增加 publicPath: './',  即可,如图  element ui字体图标不显 ...

  2. [Vuejs] webpack+vue-cli打包如何引用相对路径

    默认情况下通过webpack+vuec-li打包的css.js等资源,路径都是绝对的,即static在根目录下,假如部署到带有文件夹目录的项目中,资源路径就会出错,如何解决. 1.修改资源引用相对路径 ...

  3. webpack打包jquery并引用

    一,引入webpack插件 //打包第三方 const CommonsChunkPlugin = require("webpack/lib/optimize/CommonsChunkPlug ...

  4. vue+webpack静态资源路径引用

    处理静态资产 你可能已经注意到,在项目结构中我们有两个静态资产目录:src/assets和static/.他们之间有什么区别? 要回答这个问题,我们首先需要了解Webpack如何处理静态资产.在*.v ...

  5. 对vue-router的研究--------------引用

    pushState/replaceState/popstate 解析 HTML5提供了对history栈中内容的操作.通过history.pushState/replaceState实现添加地址到hi ...

  6. 对webpack的初步研究8

    模块 编辑文档 在模块化编程中,开发人员将程序分解为称为模块的离散功能块. 每个模块的表面积小于完整程序,使验证,调试和测试变得微不足道.编写良好的模块提供了可靠的抽象和封装边界,因此每个模块在整个应 ...

  7. webpack 3.X研究

    目前webpack已经到了3.X版本,为了方便通过最新版本打包整个前端应用,现在开始对webpack操作进行简单介绍. 一.webpack安装 在安装webpack之前需要注意,webpack是通过n ...

  8. 对JS继承的研究--------------引用

    问:类继承和原型继承不是同一回事儿吗,只是风格选择而已? 答:不是! 类继承和原型继承不论从本质上还是从语法上来说,都是两个截然不同的概念. 二者之间有着区分彼此的本质性特征.要完全看懂本文,你必须牢 ...

  9. 对数据劫持 OR 数据代理 的研究------------引用

    数据劫持,也叫数据代理. 所谓数据劫持,指的是在访问或者修改对象的某个属性时,通过一段代码拦截这个行为,进行额外的操作或者修改返回结果.比较典型的是 Object.defineProperty() 和 ...

随机推荐

  1. 代码中 方法 处提示:This method has a constructor name

    “此方法具有构造方法的名字” package classpackage; public class Puppy { public void Puppy(String name) { System.ou ...

  2. java如何防止反编译(转)

    出处: java如何防止反编译 一些防止java代码被反编译的方法 综述(写在前面的废话) Java从诞生以来,其基因就是开放精神,也正因此,其可以得到广泛爱好者的支持和奉献,最终很快发展壮大,以至于 ...

  3. Contains Duplicate III -leetcode

    Contains Duplicate III Given an array of integers, find out whether there are two distinct indices i ...

  4. Python验证数据的抽样分布类型

    假如要对一份统计数据进行分析,一般其来源来自于社会调研/普查,所以数据不是总体而是一定程度的抽样.对于抽样数据的分析,就可以结合上篇统计量及其抽样分布的内容,判断数据符合哪种分布.使用已知分布特性,可 ...

  5. SSM(Spring+SpringMVC+MyBatis)高并发优化思路

    SSM(Spring+SpringMVC+MyBatis)框架集由Spring.MyBatis两个开源框架整合而成(SpringMVC是Spring中的部分内容).常作为数据源较简单的web项目的框架 ...

  6. linux常用的bash指令

    文本处理 awk sed grep sort uniq cat cut echo fmt tr nl egrep fgrep wc 进程监视 ps top htop atop lsof 网络 nmap ...

  7. python之uWSGI和WSGI

    WSGI协议 首先弄清下面几个概念:WSGI:全称是Web Server Gateway Interface,WSGI不是服务器,python模块,框架,API或者任何软件,只是一种规范,描述web ...

  8. Redis-Hash常用命令

    Redis-Hash常用命令 hset key field value 设置一个散列,但是在散列中一次只能设置一个属性,如果要批量设置多个属性,则需要使用 hmset命令 hget key field ...

  9. Windows去除开始菜单图标背景

    1.开始菜单图标右键找到目标程序的存储目录. 2.删除目录下的目标程序名+.VisualElementsManifest.xml的文件. 3.开始菜单图标右键找到图标的存储目录,取消开始屏幕固定并删除 ...

  10. Nginx 无法重启

    报错如下 Starting nginx... nginx (pid)already running. 重启nginx时,说多个进程已存在,,, 执行 ps -ef | grep nginx 发现 有多 ...