tree shaking
当引入一个模块的时候,只引入 我们使用过的代码,那些没引用过的代码,我们就不打包了 另外 tree shaking 只支持 es module ,import模块的引入。 不支持 common js 的引入。
如果是生产模式
mode: 'production',
那么这个tree shaking 默认就是打开的。
另外要注意 要在package.json 这边 额外添加
"sideEffects": [
"*.css"
]
假设你引入了一个css 文件,却没有明确使用它,默认情况下就把css 文件也去除掉。这可能并不是我们想要的结果,所以在这里要进行额外处理。另外 babel-polyfill的实现机制是将转换过的es5代码挂载在window对象里面。 所以如果你用了babel,这里也要额外进行配置。
如果你是在 开发模式下打包,那么想要开启 tree shaking 模式 则额外需要在webpack config 文件中添加配置:
optimization: {
usedExports: true
},
development 与 production 区分打包
之前我们的打包文件只有一个。 不管是dev还是pro 都在一个config文件里面打包。其实是很不方便的, 比如 dev模式下 我们需要hmr, pro不需要, dev模式下需要webdevserver,pro也不需要, dev模式下的sourcemap 更加严格,pro模式下更加宽松。等等。
为了使用起来,我们需要将dev模式和pro 模式下的配置 区分到2个文件内, 并且考虑到这2个模式下虽然有一些配置项是不同的,但是还有大部分模块配置是相同的,所以还需要第三个文件来放这些一样的配置。
形如:
为了能让webpack支持 这种 配置文件 可以 索引到基础文件的情况,我们还需要额外安装插件
cnpm install webpack-merge -D
然后修改一下package.json
"scripts": {
"build": "webpack --config webpack.prod.js",
"start": "webpack-dev-server --config webpack.dev.js"
},
这样就实现了 dev和pro 分别打包 分别引用不一样的配置文件。然后我们最后看一下 对应的配置文件怎么写
webpack dev js :
const webpack = require('webpack')
const merge = require('webpack-merge')
const commonConfig = require('./webpack.common.js')
const devConfig = {
//开发者模式默认会打开src 为开发者目录
mode: 'development',
devtool: 'source-map',
devServer: {
contentBase: './dist',
open: true,
hot: true, //开启hmr 模式
hotOnly: true //就算hmr 没有生效 也不刷新浏览器
},
plugins: [new webpack.HotModuleReplacementPlugin()],
}
module.exports=merge(commonConfig,devConfig)
webpack pro js:
const merge = require('webpack-merge')
const commonConfig = require('./webpack.common.js')
const proConfig = {
//开发者模式默认会打开src 为开发者目录
mode: 'production',
devtool: 'cheap-module-source-map',
}
module.exports=merge(commonConfig,proConfig)
最后看一下 公共的 webpack common js :
const HtmlWebpackPlugin = require('html-webpack-plugin');
const path = require('path');
const CleanWebpackPlugin = require('clean-webpack-plugin');
module.exports={
entry: {
main: './src/index.js'
},
module: {
rules: [{
test: /\.(jpg|png|gif)$/,
use: {
loader: 'url-loader',
options: {
name: '[name]_[hash].[ext]',
outputPath: 'images/',
limit: 10240
}
}
}, {
test: /\.(eot|ttf|svg)$/,
use: {
loader: 'file-loader'
}
}, {
test: /\.scss$/,
use: [
'style-loader',
{
loader: 'css-loader',
options: {
importLoaders: 2
}
},
'sass-loader',
'postcss-loader'
]
}, {
test: /\.css$/,
use: [
'style-loader',
'css-loader',
'postcss-loader'
]
}, {
test: /\.js$/, exclude: /node_modules/, loader: "babel-loader", options: {
presets: [["@babel/preset-env", {
useBuiltIns: 'usage'
}]]
// "plugins": [["@babel/plugin-transform-runtime"],{
// "corejs": 2,
// "helpers": true,
// "regenerator": true,
// "useESModules": false,
// }]
}
}]
},
output: {
filename: '[name].js',
path: path.resolve(__dirname, 'dist')
},
plugins: [new HtmlWebpackPlugin({
template: 'src/index.html'
}), new CleanWebpackPlugin(['dist'])]
}
code splitting
lodash 这个组件 应该大家都经常用。 很方便。 但是有一个问题就是,当我们引用这个组件的时候 包大小不好控制。 且每次有更新都得重新下载。影响界面性能。 举例说明:
- 不管你用了多少个函数,但是整个lodash 都会被打包 到最终js文件里面
- 我们有时只是改了一下 业务代码的传参,重新打包以后 对于用户来说 就是需要将整个js文件全部重新下载。
例如
import _ from 'lodash'
console.log(_.join(['a','b','c'],''))
假设我们的代码是这么写的,当我们某个版本 输出结果想要改成 abd的时候,实际上我们只改了一个参数,但是因为我们打包出来只有一个文件,所以对于用户来讲,他就是全量更新了一次。
为了解决这个问题 我们可以:
1.再写一个lodash文件
2.然后这个lodash文件的 作用 只是挂载一下 而已
import _ from 'lodash'
//把lodash 挂载到window 对象上
window._ = _;
// console.log(_.join(['a','b','c'],'adasda'))
- 我们的index 文件就可以修改为:
console.log(window._.join(['a','b','c'],''))
然后修改一下 我们的 webpack config 文件
entry: {
lodash: './src/lodash.js',
main: './src/index.js'
},
如此一来,再次打包就变成
现在的情况就好很多了,因为拆分成了2个js文件,所以并行加载2个文件 肯定是比我们串行加载一个文件要快的,而且 对于我们的业务代码模块 main.js来说 ,以后不管修改多少次,用户都只会重新load 这个main.js 而不会去load 这个lodash.js文件。
有那么一点点 增量更新的意思
所以code splitting 就是代码拆分,我们没有webpack也可以这么做 来提高页面的性能。但是有了webpack以后我们来做code splitting就会非常简单。
有了webpack以后 要做代码拆分 那就太简单了 只需要再webpack的配置文件中增加:
optimization: {
splitChunks: {
chunks: 'all'
}
},
其他的不用变,所有代码拆分的工作 webpack都帮你做好了
上面的例子是同步的,也就是说 对于浏览器而言,我们是先加载的loadsh js文件,然后再调用我们的业务方法。
那么还有一种情况是异步的,我们来看看 异步的情况怎么处理
function getComponent() {
//异步加载 lodash 库
return import('lodash').then(
({ default: _ }) => {
var element = document.createElement('div')
element.innerHTML = _.join(['a', 'b', 'c'], " ")
return element;
}
)
}
getComponent().then(element => {
document.body.appendChild(element)
})
也是可以代码分割的:
之前我们看到动态加载的js库的名字 命名是0.js,这样看起来其实是不太好的。我们想要实际的名字这样看起来更有意义。 这里就需要用到魔法注释的功能
function getComponent() {
//异步加载 lodash 库
return import(/* webpackChunkName:"lodash" */'lodash').then(
({ default: _ }) => {
var element = document.createElement('div')
element.innerHTML = _.join(['a', 'b', 'c'], " ")
return element;
}
)
}
注意看中间的注释,这个注释加上去以后 再次打包:
这样我们的动态加载的js库的名字 也有了一定的意义。 当然这里我们想做的极限一些 就是不要vendors这个前缀。 看看官网的配置 修改我们的webpack-config js文件
optimization: {
splitChunks: {
chunks: 'all',
cacheGroups: {
vendors: false,
default: false
}
}
},
然后再次打包
现在动态加载的库 也正常了。
这些类似懒加载的功能 实际运用起来 效果还是不错的。就是用到的时候才下载, 不用的时候不要下载, 可以有效提升页面加载速度。 这与 vue react 等框架里面的 router的作用有一些类似。
懒加载的特性 跟 import 语法 强相关
唯一要注意的是,在使用类似的技术的时候 一定要记得使用babel等类似的库 做一下es6 to es5 转码的工作。
webpack 打包分析
修改我们的package.json文件。
"scripts": {
"build": "webpack --config webpack.prod.js",
"start": "webpack-dev-server --config webpack.dev.js",
//主要就是看这里,输出了一个stats的json文件
"dev": "webpack --profile --json > stats.json --config webpack.dev.js"
},
再次打包以后 我们就会看到这个文件了
将这个文件 上传到 官网分析器
看看官网的分析结果:
我们也可以用一些插件来完成 打包过程的分析: webpack-bundle-analyzer 这个是比较常用的
prefetchingpreloading (重要)
optimization: {
splitChunks: {
chunks: 'all'
}
},
这个all的含义就是 不管是同步的代码 还是 异步的代码 webpack都会对他 做代码分割 如果你不指定这个值得话 他的默认值是async,也就是说 默认情况下 只有异步的代码才会做代码分割。
看一段简单的js代码:
document.addEventListener('click', () => {
const element = document.createElement('div')
element.innerHTML = 'wuyue'
document.body.appendChild(element)
})
作用就是点击一下页面 就新增一个div 内容为wuyue的节点。 那么这段代码在我们打包后的main.js里面。 但对于 我们的用户来说 main.js 东西很多,但实际起作用的 只有这么一段代码。且这段代码也是点击以后才起作用的,不点击的话 实际上这段代码也没啥用。属于一个废加载。
可以打开chrome 看一下 这个coverage 面板
当我们重新刷新页面的时候 可以看出来 这个main.js 的利用率是78.2。
看这里
这段代码的标记是红色的,也就是说 这段代码 在页面首次加载的时候是用不到的。
那么webpack 有没有更好的方式能规避掉 这个影响页面性能的东西呢? 答案是有的。
我们新建一个click.js 文件
function handleClick(){
const element =document.createElement('div')
element.innerHTML='wuyue'
document.body.appendChild(element)
}
export default handleClick
然后修改一下index.js
document.addEventListener('click', () => {
import('./click.js').then(({ default: func }) => {
func()
})
})
打包以后 再看看我们的代码使用率:
使用率 提升了一个点。 而且我们看看 这个新的listener 里面的代码 和我们之前的代码是不一样的。
我们这个时候点击屏幕
可以看出来,点击以后 这段js代码才会被下载。
所以 webpack本质上 是希望你 多写一些异步加载的代码,这样才可以增加你页面的运行速度。 这也就是为什么 webpack 默认的split的配置是async 因为webpack认为你只有这样(异步加载)才能真正加快你网页运行的速度,而同步加载的split最多也就是启到一个缓存优化的作用,对于首次加载是没什么作用的
这里有人又要问了:
如果你做成这样的异步下载,那么对于用户的点击事件而言,他点了一个按钮以后 是不是要等待一段事件才有反应?因为毕竟 这么做 以后 相关的代码都是点击以后才会下载下来的。
这确实是一个问题,同时这也是这一个小节 想要解决的问题。
比如你看一下这个网站,首页有个登录框。默认是不展示的。 当用户点击了 登录按钮以后 就会展示 这个模块了
所以我们想把登录部分的js代码 用异步加载的方式来优化。也就是默认打开这个页面 登录的js代码不下载, 但是当点击登录按钮以后 这部分js代码才下载。
但是这个逻辑上面已经说了,点击以后才下载 体验上可能会更慢一些。
所以这里唯一的解决方案就是 当页面首次加载以后空闲的时候 再去加载这个登录的js 代码。
这样既不会影响页面首次加载的速度,也不会影响点击登录按钮以后的反应。
这时候就要依赖官方的prefetchingpreloading模块了
一般情况下 我们用prefetch比较多。
用他也很简单。
加这么一点魔法注释即可
document.addEventListener('click', () => {
import(/* webpackPrefetch: true */'./click.js').then(({ default: func }) => {
func()
})
})
然后再次刷新我们的页面
可以看出来 main.js加载完毕以后 才去加载0.js
总结:
利用缓存来提升页面性能的手段实际上提升的效果十分有限,而提高js代码的利用率则效果非凡,利用好webpack提供给我们的prefetch这个特性能够极大的提升页面渲染的性能。
css 代码分割
webpack 默认情况下 会把css代码 也打包到js 文件里面
如图:
使用该插件以后:
当然这里的css代码 还没有进行压缩 可以使用这个插件 来对css代码进行压缩
浏览器缓存 与webpack
如图:
output: {
filename: '[name].[contenthash].js',
path: path.resolve(__dirname, 'dist')
},
加上contenthash以后 才会 每次打包(当且仅当代码有变化时,hash值才发生变化)有一个hash值
否则是不会有的。
这样就能保证代码有修改以后 每次浏览器访问的代码都是最新的代码 因为文件名变更了。否则的话 每次打包出来的名称都一样 会导致浏览器一直使用缓存文件。
当然我们这里的contenthash只要配置在pro环境中即可。
shimming
看下面这个问题:
看这个jquery ui. js 这个文件 他里面用的其实是jquery的操作符。但是 jquery我们是在index 文件中引入的。
这是因为webpack的 模块化打包造成的。要解决这个问题,我们可以在ui.js这个文件 引入一下jquery import 一下即可。
但是假设这个js 文件是一个第三方库,我们不想改动他的代码,这种情况应该怎么办?
新增一下这个ProvidePlugin 即可
plugins: [new HtmlWebpackPlugin({
template: 'src/index.html'
}), new CleanWebpackPlugin(['dist']), new MiniCssExtractPlugin(), new webpack.ProvidePlugin({
$: 'jquery' //发现 有$ 这个字符串 就自动引入jquery 这个库
})],
shimming 这个东西 在我们引用一些老的第三方库的时候会经常遇到,有需要的同学这里建议通读一遍官方文档