先看实践效果
一、业务背景:
- 秒开率目标
- 目前运营活动中的CSS文件是犬粮打包进js文件中
- 优点:是方便管理,减少首次请求数量
- 缺点:首次加载时需要下载整个JS文件增加首次加载时间,以及无法使用css缓存;不能和js并行下载;
- 尤其目前我们业务中有一个比较大的公共style文件,页面每个组件中都需要单独引入;
- 项目中没有进入css的treeShaking;
针对html没进行压缩的问题,我们引进html的压缩机制
const minifyHtmlPlugins = (pages) => {
return Object.values(pages).map((page) => {
return new HtmlWebpackPlugin({
template: page.template,
filename: page.filename,
minify: {
minifyCSS: true, // 压缩内联CSS
minifyJS: true, // 压缩内联JS
collapseWhitespace:true,
keepClosingSlash:true,
removeComments: true, // 移除注释
removeRedundantAttributes:true,
removeScriptTypeAttributes:true,
removeStyleLinkTypeAttributes:true,
useShortDoctype: true
},
})
})
}
configureWebpack: config => {
config.stats = "normal";
config.module.rules.push({
test: /banners/[\w$/]+/main.js$/,
use: ["./src/plugins/release.loader.js", "./src/plugins/hoc.loader.js"]
});
config.plugins.push(...minifyHtmlPlugins(pages))
}
二、优化方向
1. 将`src/style`中的公共样式打包成一个单独的css文件压缩并上传到CDN,link到html中进行预加载;
2. 设置全局stylus变量
3. css拆分,首屏css内联(Critial CSS);其他css延迟加载(或者按需加载)
4. 非首屏css延迟加载(或者按需加载)
5. 引入CSS的treeShaking
三、 具体实施
1. 公共CSS上传CDN并进行预先加载
在项目中,我以在css单独抽离css中的extract配置打开
打包出来的结果搜索全局公共样式fz-28
里面有8个;整个css文件有381KB
; 通过查看代码发现,我们在项目中是这样使用style的
// main.js中
import { setFontSize } from '@/rem';
import '@/directive/preventReClick';
import '@/style/wepie.styl'
// 每个组件的style中
@import '~@/style/stylusSet/index.styl';
.btn-rule
wh 100
left 7.7rem
right 1rem
// 但是在'~@/style/stylusSet/index.styl'中又引入了原生重算
// 原生重算
@import "./reset.styl"
@import "../wepie.styl";
@import "../partials/reset.styl"
@import "../easy.styl"
所以也就是最终我们引用n次~@/style/stylusSet/index.styl
,加上main.js中的1个,最终重复的公共代码的总次数就是n+1
次
优化一:改写重制部分部分引入;
所以第一个优化就是去除~@/style/stylusSet/index.styl
中的原生重算部分;改用只在main.js中引入;这时重新打包后发现大小缩减至270KB
;
优化二:全局公共代码上传CDN,并进行预加载
2. 设置全局stylus变量
项目中用的stylus,里面封装了一套适配项目的一些简化css编写的函数,如设置背景图的工具函数如下:
在使用时,我们需要在页面中引入该函数,如下所示
每次都要引入; 可以释放这部分工作
module.exports = {
css: {
...,
loaderOptions: {
stylus: {
// 设置全局stylus变量
import: [
'~@/style/stylusSet/index.styl',
]
}
}
}
}
3. CSS拆分,主屏CSS内联
但是问题是从js中剥离后,意味着页面加载需要多发一个http请求;所以想到另外一个替代方式,就是css内联
CSS内联需要使用html-inline-css-webpack-plugin;
【原理】: 将CSS打包成单独Chunk; 遍历所有的css-chunk,将其复制
设置index.html的header头下(默认
);然后(默认
)删除掉所有的css-chunk;
这种简单粗暴的实现方式有几个隐藏的问题,后面讲到
3.1 引入时要注意有default
const HTMLInlineCSSWebpackPlugin = require("html-inline-css-webpack-plugin").default;
- 官方文档webpack中配置如下
3.2 在vue-cli中写法
但是实际在我们的vue-cli
中并不能这样配置,因为里面已经有很多默认配置了;可以通过vue inspect > webpack.config.js
查看
- 必须将css打包成单独chunk
module.exports = {
css: {
extract: true,
}
}
多页面HtmlWebpackPlugin优化处理
const minifyHtmlPlugins = (pages) => {
return Object.values(pages).map((page) => {
return new HtmlWebpackPlugin({
template: page.template,
filename: page.filename,
chunks: page.chunks,
inject: true,
minify: {
html5: true,
minifyCSS: true, // 压缩内联CSS
minifyJS: true, // 压缩内联JS
collapseWhitespace:true,
keepClosingSlash:true,
removeComments: true, // 去除注释
removeRedundantAttributes:true,
removeScriptTypeAttributes:true,
removeStyleLinkTypeAttributes:true,
useShortDoctype: true
},
})
})
}
css内联到html中
module.exports = {
configureWebpack: config => {
...,
// 单独提取css文件(这里添加后会导致HTMLInlineCSSWebpackPlugin中有2份样式)
// new MiniCssExtractPlugin({
// filename: "[name].css",
// // chunkFilename: "[id].css"
// }),
config.plugins.push(...[
// 压缩html以及里面的css/js等
...minifyHtmlPlugins(pages),
// 将单独提取的css文件内联到html中(这行要放在HtmlWebpackPlugin后)
new HTMLInlineCSSWebpackPlugin()
])
}
}
上述注视注意,vue-cli.js中通过开启css.extract = true
时已经处理了MiniCssExtractPlugin
; 这里如果再写,将会导致html中copy两份一样的css;所以不要再写;
3.3 问题第一弹
通过build
本地打包,并启动http-server
后,预览页面,发现页面中样式图片路径均是404
审查html中的样式代码发现如下
里面图片路径均是../
这里我们执行vue inspect --mode product > webpack.config.js
查看到css-chunk默认生成路径在css文件夹下
同时查看,默认设置的单独提取css中设置的publicPath为'../'
这里我这边做测试修改; 直接在plugin中试图覆盖
再次执行vue inspect --mode product > webpack.config.js
命令查看改动没有生效;所以得换方式
module.exports = {
chainWebpack: config => {
config.plugin('extract-css')
.use(MiniCssExtractPlugin, [
{
filename: '[name].[contenthash:8].css',
chunkFilename: '[name].[contenthash:8].css'
}
]);
}
}
再次执行vue inspect --mode product > webpack.config.js
已经更新成功
BUT 执行build
时报错:Error: No module factory available for dependency type: CssDependency
; 原因待查……
这里尝试不改变css生成的路径,直接改css中引入的背景图路径的法子; 如上直接改publicPath
module.exports = {
chainWebpack: config => {
config.module
.rule('stylus')
.oneOf('vue')
.use('extract-css-loader')
.tap(options => {
options.publicPath = '/'
return options;
});
}
}
}
执行vue inspect --mode product > webpack.config.js
和执行build
后图片路径变成如下
4. 非首屏css按需加载
按需加载需要vue写的时候用组件异步加载的写法写;
const GuardTreasure = () => import('./components/treasure/guard-treasure.vue')
这样引入的css文件会被link到JavaScript文件中,在运行时需要时加载,而不会inline到html中;
4.1 问题第二弹
但是我们这样写了后发现异步组件加载时会报https://xxxxx/css/chunk-0950ed4p.css
404;
查找原因就是因为先前提到的html-inline-css-webpack-plugin
原理中,在copy了依赖的CSS后,在删除时是将整个CSS文件夹删除,导致异步组件中link的css-chunk也被意外删除;
看源码以及官网说明,我们绝对直接保留css文件夹;这样唯一的后果就是打包后的dist/css中多了一些已经被inline到html中的无用样式,浪费存储,但其实并不会引入和读取,没有其他副作用了; 所以我们要设置一个属性
new HTMLInlineCSSWebpackPlugin({
leaveCSSFile: true,
}),
5. CSS的Tree Shaking 擦除无用的样式
const PurgeCSSPlugin = require('purgecss-webpack-plugin')
module.exports = {
chainWebpack: config => {
config.plugin.push(
new PurgeCSSPlugin({
paths: globAll.sync(
[...Object.keys(pages).map(pageDirName => `${path.join(__dirname, '../src')}/banners/${pageDirName}/**/*`),`${path.join(__dirname, '../src')}/comps/**/*`],
{ nodir: true }
),
// paths: glob.sync(`${path.join(__dirname, '../src')}/banners/${pageDirName}/**/*`, { nodir: true }),
whitelist: ['webp'], // 白名的
whitelistPatterns: [/^kp/], // 以kp-开头的 不擦除
})
)
}
}