webpack打包经验——处理打包文件体积过大的问题
前言
最近对一个比较老的公司项目做了一次优化,处理的主要是webpack打包文件体积过大的问题。
这里就写一下对于webpack打包优化的一些经验。
主要分为以下几个方面:
- 去掉开发环境下的配置
- ExtractTextPlugin:提取样式到css文件
- webpack-bundle-analyzer:webpack打包文件体积和依赖关系的可视化
- CommonsChunkPlugin:提取通用模块文件
- 提取manifest:让提取的公共js的hash值不要改变
- 压缩js,css,图片
- react-router 4 之前的按需加载
- react-router 4 的按需加载
- react v16.6之后 的按需加载(2019.07.04更新)
本篇博客用到的webpack插件如何配置都可以去查看我写的这篇博客:
【Webpack的使用指南 02】Webpack的常用解决方案
这里就不细讲这些配置了。
去掉开发环境下的配置
比如webpack中的devtool改为false,不需要热加载这类只用于开发环境的东西。
这些不算是优化,而算是错误了。
对于在开发环境下才有用的东西在打包到生产环境时通通去掉。
ExtractTextPlugin:提取样式到css文件
将样式提取到单独的css文件,而不是内嵌到打包的js文件中。
这样带来的好处时分离出来的css和js是可以并行下载的,这样可以更快地加载样式和脚本。
解决方案:
安装ExtractTextPlugin
npm i --save-dev extract-text-webpack-plugin
然后修改webpack.config.js为:
const ExtractTextPlugin = require('extract-text-webpack-plugin');
module.exports = {
// ...
plugins: [
// ...
new ExtractTextPlugin({ filename: '[name].[contenthash].css', allChunks: false }),
],
module: {
rules: [
{
test: /\.css$/,
exclude: /node_modules/,
use: ExtractTextPlugin.extract({
fallback: 'style-loader',
use: ['css-loader?modules', 'postcss-loader'],
}),
}, {
test: /\.css$/,
include: /node_modules/,
use: ExtractTextPlugin.extract({
fallback: 'style-loader',
use: ['css-loader', 'postcss-loader'],
}),
},
{
test: /\.less$/,
use: ExtractTextPlugin.extract({
fallback: 'style-loader',
use: ['css-loader?modules', 'less-loader', 'postcss-loader'],
}),
},
],
},
}
打包后生成文件如下:
webpack-bundle-analyzer:webpack打包文件体积和依赖关系的可视化
这个东西不算是优化,而是让我们可以清晰得看到各个包的输出文件体积与交互关系。
安装:
npm install --save-dev webpack-bundle-analyzer
然后修改webpack.config.js:
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
module.exports = merge(common, {
// ...
plugins: [
new BundleAnalyzerPlugin({ analyzerPort: 8919 })
],
});
打包后会自动出现一个端口为8919的站点,站点内容如下:
可以看到我们打包后的main.js中的代码一部分来自node_modules文件夹中的模块,一部分来自自己写的代码,也就是src文件夹中的代码。
为了之后描述方便,这个图我们直接翻译过来就叫webpack打包分析图。
CommonsChunkPlugin:提取通用模块文件
所谓通用模块,就是如react,react-dom,redux,axios几乎每个页面都会应用到的js模块。
将这些js模块提取出来放到一个文件中,不仅可以缩小主文件的大小,在第一次下载的时候能并行下载,提高加载效率,更重要的是这些文件的代码几乎不会变动,那么每次打包发布后,仍然会沿用缓存,从而提高了加载效率。
而对于那些多文件入口的应用更是有效,因为在加载不同的页面时,这部分代码是公共的,直接可以从缓存中应用。
这个东西不需要安装,直接修改webpack的配置文件即可:
const webpack = require('webpack');
module.exports = {
entry: {
main: ['babel-polyfill', './src/app.js'],
vendor: [
'react',
'react-dom',
'redux',
'react-router-dom',
'react-redux',
'redux-actions',
'axios'
]
},
plugins: [
new webpack.optimize.CommonsChunkPlugin({
names: ['vendor'],
minChunks: Infinity,
filename: 'common.bundle.[chunkhash].js',
})
]
}
打包后的webpack打包分析图为:
可以很明显看到react这些模块都被打包进了common.js中。
提取manifest:让提取的公共js的hash值不要改变
当我们了解webpack中的hash值时,一般都会看到[hash]和[chunkhash]两种hash值的配置。
其中hash根据每次编译的内容计算得到,所以每编译一次所有文件都会生成一个新的hash,也就完全无法利用缓存。
所以我们这里用了[chunkhash],chunkhash是根据内容来生成的,所以如果内容不改变,那么生成的hash值就不会改变。
chunkhash适用于一般的情况,但是,对于我们以上的情况是不适用的。
我去改变主文件代码,然后生成的两个公共js代码的chunkhash值却改变了,它们并没有使用到主文件。
于是我用文本对比工具,对比了它们的代码,发现只有一行代码是有差别的:
这是因为webpack在执行时会有一个带有模块标识的运行时代码。
当我们不提取vendor包的时候这段代码会被打包到main.js文件中。
当我们提取vendor到common.js时,这段脚本会被注入到common.js里面,而main.js中没有这段脚本了了.
当我们将库文件分为两个包提取出来,分别为common1.js和common2.js,发现这段脚本只出现在一个common1.js中,并且
那段标识代码变成了:
u.src=t.p+""+e+"."+{0:"9237ad6420af10443d7f",1:"be5ff93ec752c5169d4c"}
然后发现其他包的首部都会有个这样的代码:
webpackJsonp([1],{2:functio
这个运行时脚本的代码正好和其他包开始的那段代码中的数字相对应。
我们可以将这部分代码提取到一个单独的js中,这样打包的公共js就不会受到影响。
我们可以进行如下配置:
plugins: [
new webpack.optimize.CommonsChunkPlugin({
names: ['vendor'],
minChunks: Infinity,
filename: 'common.bundle.[chunkhash].js',
}),
new webpack.optimize.CommonsChunkPlugin({
names: ['manifest'],
filename: 'manifest.bundle.[chunkhash].js',
}),
new webpack.HashedModuleIdsPlugin()
]
对于names而言,如果chunk已经在entry中定义了,那么就会根据entry中的入口提取chunk文件。如果没有定义,比如mainifest,那么就会生成一个空的chunk文件,来提取其他所有chunk的公共代码。
而我们这段代码的意思就是将webpack注入到包中的那段公共代码提取出来。
打包后的文件:
webpack打包分析图:
看到图中绿色的那个块了吗?
那个东西就是打包后的manifest文件。
这样处理后,当我们再修改主文件中的代码时,生成的公共js的chunkhash是不会改变的,改变的是那个单独提取出来的manifest.bundle.[chunkhash].js的chunkhash。
压缩js,css,图片
这个其实不准备记录进来,因为这些一般项目应该都具备了,不过这里还是顺带提一句吧。
压缩js和css一步即可:
webpack -p
图片的压缩:
image-webpack-loader
具体的使用请查看 Webpack的常用解决方案 的第16点。
react-router 4 之前的按需加载
如果使用过Ant Design 一般都知道有一个配置按需加载的功能,就是在最后打包的时候只把用到的组件代码打包。
而对于一般的react组件其实也有一个使用react-router实现按需加载的玩法。
对于每一个路由而言,其他路由的代码实际上并不是必须的,所以当切换到某一个路由后,如果只加载这个路由的代码,那么首屏加载的速度将大大提升。
首先在webpack的output中配置
output: {
// ...
chunkFilename: '[name].[chunkhash:5].chunk.js',
},
然后需要将react-router的加载改为按需加载,例如对于下面这样的代码:
import React from 'react';
import { createStore } from 'redux';
import { Provider } from 'react-redux';
import ReactDOM from 'react-dom';
import { HashRouter as Router, Route } from 'react-router-dom';
import PageMain from './components/pageMain';
import PageSearch from './components/pageSearch';
import PageReader from './components/pageReader';
import reducer from './reducers';
const store = createStore(reducer);
const App = () => (
<Provider store={store}>
<Router>
<div>
<Route exact path="/" component={PageMain} />
<Route path="/search" component={PageSearch} />
<Route path="/reader/:bookid/:link" component={PageReader} />
</div>
</Router>
</Provider>
);
应该改为:
import React from 'react';
import { createStore } from 'redux';
import { Provider } from 'react-redux';
import ReactDOM from 'react-dom';
import { HashRouter as Router, Route } from 'react-router-dom';
import reducer from './reducers';
const store = createStore(reducer);
const PageMain = (location, callback) => {
require.ensure([], require => {
callback(null, require('./components/pageMain').default);
}, 'PageMain');
};
const PageSearch = (location, callback) => {
require.ensure([], require => {
callback(null, require('./components/pageSearch').default);
}, 'PageSearch');
};
const PageReader = (location, callback) => {
require.ensure([], require => {
callback(null, require('./components/pageReader').default);
}, 'PageReader');
};
const App = () => (
<Provider store={store}>
<Router>
<div>
<Route exact path="/" getComponent={PageMain} />
<Route path="/search" getComponent={PageSearch} />
<Route path="/reader/:bookid/:link" getComponent={PageReader} />
</div>
</Router>
</Provider>
);
react-router 4 的按需加载
上面那种方法应用到react-router 4上是行不通的,因为getComponent方法已经被移除了。
然后我参考了官方教程的方法
在这里我们需要用到webpack, babel-plugin-syntax-dynamic-import和 react-loadable。
webpack内建了动态加载,但是我们因为用到了babel,所以需要去用babel-plugin-syntax-dynamic-import避免做一些额外的转换。
所以首先需要
npm i babel-plugin-syntax-dynamic-import --save-dev
然后在.babelrc加入配置:
"plugins": [
"syntax-dynamic-import"
]
接下来我们需要用到react-loadable,它是一个用于动态加载组件的高阶组件。
这是官网上的一个例子
import Loadable from 'react-loadable';
import Loading from './my-loading-component';
const LoadableComponent = Loadable({
loader: () => import('./my-component'),
loading: Loading,
});
export default class App extends React.Component {
render() {
return <LoadableComponent/>;
}
}
使用起来并不难,Loadable函数会传入一个参数对象,返回一个渲染到界面上的组件。
这个参数对象的loader属性就是需要动态加载的组件,而loading这个属性传入的是一个展示加载状态的组件,当还没有加载出动态组件时,展示在界面上的就是这个loading组件。
使用这种方法相对于原来的方式优势很明显,我们不只是在路由上可以进行动态加载了,我们动态加载的组件粒度可以更细,比如一个时钟组件,而不是像之前那样往往是一个页面。
通过灵活去使用动态加载可以完美控制加载的js的大小,从而使首屏加载时间和其他页面加载时间控制到一个相对平衡的度。
这里有个点需要注意,就是通常我们在使用loading组件时经常会出现的问题:闪烁现象。
这种现象的原因是,在加载真正的组件前,会出现loading页面,但是组件加载很快,就会导致loading页面出现的时间很短,从而造成闪烁。
解决的方法就是加个属性delay
const LoadableComponent = Loadable({
loader: () => import('./my-component'),
loading: Loading,
delay: 200
});
只有当加载时间大于200ms时loading组件才会出现。
还有更多的关于react-loadable的玩法:https://www.npmjs.com/package/react-loadable
那么现在看下我们的打包文件:
webpack打包分析图:
注意看看上面的打包文件名字,发现通过这种方法进行按需加载的几个文件都是按照数字命名,而没有按照我们期望的组件名命名。
我在这个项目的github上面找了一下,发现它提供的按组件命名的方法需要用到服务端渲染,然后就没有继续下去了。
反正这个东西也不是很重要,所以就没有进一步深究,如果有园友对这个问题有好的办法,也希望能在评论里说明。
react v16.6之后 的按需加载(2019.07.04更新)
React这个版本新加了lazy和Suspense这两个功能。
对于上面的按需加载,可以修改代码为:
import React, { Suspense } from 'react';
import Loading from './my-loading-component';
const LoadableComponent = React.lazy(() => import('./my-component'));
export default class App extends React.Component {
render() {
return (
<Suspense fallback={<Loading />}>
<LoadableComponent/>;
</Suspense>
)
}
}
临时更新,写得简单点,见谅!
总结
总的来讲,通过以上步骤应该是可以解决绝大多数打包文件体积过大的问题。
当然,因为文中webpack版本和插件版本的差异,在配置和玩法上会有一些不同,但是上面描述的这些方向都是没有问题的,并且相信在各个版本下都可以找到相应的解决方案。
文中如有疑误,请不吝赐教。
webpack打包经验——处理打包文件体积过大的问题的更多相关文章
- 解决 webpack 打包文件体积过大
webpack 把我们所有的文件都打包成一个 JS 文件,这样即使你是小项目,打包后的文件也会非常大.下面就来讲下如何从多个方面进行优化. 去除不必要的插件 刚开始用 webpack 的时候,开发环境 ...
- asp.net core 发布到 docker 容器时文件体积过大及服务端口的配置疑问
在 asp.net core 发布时,本人先后产生了3个疑问. 1.发布的程序为什么不能在docker容器中运行 当时在window开发环境中发布后,dotnet xxx.dll可以正常运行:但放入d ...
- 彻底解决 webpack 打包文件体积过大
http://www.jianshu.com/p/a64735eb0e2b https://segmentfault.com/q/1010000006018592?_ea=985024 http:// ...
- Python打包成exe,文件太大问题解决办法
Python打包成exe,文件太大问题解决办法 原因 解决办法 具体步骤 情况一:初次打包 情况二:再次打包 原因 由于使用pyinstaller打包.py文件时,会把很多已安装的无关库同时打包进去, ...
- 配置webpack中externals来减少打包后vendor.js的体积
在日常的项目开发中,我们会用到各种第三方库来提高效率,但随之带来的问题就是打包后的vendor.js体积过大,导致加载时空白页时间过长,给用户的体验太差.为此我们需要减少vendor.js的体积,从本 ...
- C# .NET的BinaryFormatter、protobuf-net、Newtonsoft.Json以及自己写的序列化方法序列化效率和序列化后的文件体积大小对比
测试结果如下图: 测试结果整理后: 结论: 1.这几个工具中,protobuf-net序列化和反序列化效率是最快的 2.BinaryFormatter和Newtonsoft.Json反序列化慢的比较多 ...
- webpack减少打包后文件体积的几种方法
webpack 把我们所有的文件都打包成一个 JS 文件,这样即使你是小项目,打包后的文件也会非常大.下面就来讲下如何从多个方面进行优化. 去除不必要的插件 刚开始用 webpack 的时候,开发环境 ...
- webpack解惑:多入口文件打包策略
本文是我用webpack进行项目构建的实践心得,场景是这样的,项目是大型类cms型,技术选型是vue,只支持chrome,有诸多子功能模块,全部打包在一起的话会有好几MB,所以最佳方式是进行多入口打包 ...
- 使用webpack打包ThinkPHP的资源文件
使用webpack打包ThinkPHP的资源文件 利用自己的空余时间一直在维护http://www.wx2share.com这个小网站,全是一个人在弄,由于只租得起虚拟空间,所以后台采用了简单方便的T ...
随机推荐
- 设计模式—桥接模式的C++实现
这是Bwar在2009年写的设计模式C++实现,代码均可编译可运行,一直存在自己的电脑里,曾经在团队技术分享中分享过,现搬到线上来. 1. 装饰模式简述 1.1 目的 将抽象部分与它的实现部分分离,使 ...
- python的内置函数time
time 模块 1 >>> import time 2 >>> time.time() 3 1491064723.808669 4 >>> # t ...
- Django 配置文件settings注解(含静态文件和上传文件配置)
基于Django1.11配置文件settings.py import os import sys # Build paths inside the project like this: os.path ...
- Scala类型限定
package big.data.analyse.scala /** * 类型限定 * Created by zhen on 2018/12/9. */ object Lxxd { def main( ...
- spring4笔记----UrlResource访问网络资源读取xml内容
package com.ij34.bean; import java.util.Iterator; import java.util.List; import org.dom4j.Document; ...
- c/c++ 标准库 pair 介绍
标准库 pair 介绍 问题:map里的元素由key和value组成,这个key和value的组合是什么类型呢??? 答案:pair类型 pair介绍: 它是模板 有2个公有成员可供访问. first ...
- c/c++ 有向无环图 directed acycline graph
c/c++ 有向无环图 directed acycline graph 概念: 图中点与点之间的线是有方向的,图中不存在环.用邻接表的方式,实现的图. 名词: 顶点的入度:到这个顶点的线的数量. 顶点 ...
- Win10 - MySQL-zip安装方法
Win10 - MySQL-zip安装方法 安装步骤 1.下载,到MySQL官网:https://dev.mysql.com/downloads/mysql/ 2.解压安装包 解压下载的安装包,放到你 ...
- [Hive_3] Hive 建表指定分隔符
0. 说明 Hive 建表示例及指定分隔符 1. Hive 建表 Demo 在 Hive 中输入以下命令创建表 user2 create table users2 (id int, name stri ...
- IE6浏览器无法打开QQ邮箱
原因:未启用TLS1.0 解决方法: 打开IE浏览器,依次打开 [Internet]→[高级],在 设置 选项卡中,勾选[使用TLS1.0],然后点击[确定]保存修改,重启浏览器即可.