前言
最近采用webpack5配置了一个项目脚手架。当项目开发完成后,采用lighthouse进行性能测试时发现有一张图片影响LCP评分,所以需要解决这个问题。
LCP
最大内容绘制 (LCP) 是核心 Web 指标中的一项指标,用于测量可视区域中最大内容元素变为可见的时间点。该项指标可用于确定页面主要内容在屏幕上完成渲染的时间点。
本项目经过实测发现以下问题:
/assets/top-8face.png
所以这个图片影响LCP需要优化,怎么做?这里给出了preload的建议,只要在html head标签内加上一句即可
<link href="assets/top-8face.png" rel="preload" as="image">
至此完成了该项优化。
但是,事情可能没有那么简单,万一图片换了,top-[hash]变了呢?或者随着项目迭代,删除了这张图片,然后这个preload link很可能继续存在html里面,就会显得多余。万一影响LCP的不是图片,可以是p标签包裹的一大段文案或者是某个video、svg内嵌的image呢?万一项目是动态配置的多入口MPA呢?
所以此时虽然可以手动写上一句就解决当前的问题,但是为了可维护性,从工程化角度来看,更应该是用工程化的手段来解决此类问题。
先看看有没有现成的preload插件,找到一个vue官方维护的 @vue/preload-webpack-plugin, 但是这个插件默认把所有输出的资源都生成preload link,在输出资源比较多且没有开启http2时候,这其实是一种反向优化的手段。经过尝试配置和阅读它的源码,发现并不能满足我的需求,我只要一个preload link就行了,所以自己写一个吧。
webpack插件实现新增preload link插入到html文件
梳理一下本插件要做的事情:
我们希望拿到名叫top-[hash].png的图片,有则组装一个preload link插入到html里面,没有则啥也不做
在我的另外一篇文章【webpack进阶系列】plugin的原理探究 介绍过了插件原理与应用,参考官网示例现在我们可以直接在processAssets钩子里面配合stage: Compilation.PROCESS_ASSETS_STAGE_SUMMARIZE
拿到所有资源assets,找出LCP图片和html文件,组装preload link插入到html文件,然后生成新的html。
代码如下:
preload-lcp-img-webpack-plugin.js
/**
* 根据lighthouse的建议,有一张图片:top-[hash].png影响LCP, 所以需要预加载这张影响LCP的图片,此插件仅适用此场景
*/
const defaultOptions = {
htmlFile: 'index.html'
}
class PreloadLCPImgWebpackPlugin {
constructor(options = {}) {
this.options = { ...defaultOptions, ...options }
}
apply(compiler) {
const pluginName = 'PreloadLCPImgWebpackPlugin'
const { webpack } = compiler
const { Compilation } = webpack
const { RawSource } = webpack.sources
compiler.hooks.thisCompilation.tap(pluginName, (compilation) => {
// 绑定到资源处理流水线(assets processing pipeline)
compilation.hooks.processAssets.tap(
{
name: pluginName,
// 用某个靠后的资源处理阶段, 确保所有资源已被插件添加到 compilation
stage: Compilation.PROCESS_ASSETS_STAGE_SUMMARIZE
},
(assets) => {
const lcpImg = Object.keys(assets)
.filter(filename => /.png$/i.test(filename)) // 图片
.find((filename) => {
const arr = filename.split('/')
const name = arr[arr.length - 1]
return /^top/.test(name) // top-[hash].png就是影响LCP的图片
})
if (!lcpImg) return
const webpackPublicPath = compilation.outputOptions.publicPath
// webpack 5 set publicPath default value 'auto'
const publicPath = webpackPublicPath.trim() !== '' && webpackPublicPath !== 'auto' ? webpackPublicPath : ''
const html = assets[this.options.htmlFile]._value
const preloadLink = `<link href="${publicPath}${lcpImg}" rel="preload" as="image">`
const newHtml = html.replace('</head>', `${preloadLink}</head>`)
delete assets[this.options.htmlFile] // 删除,下面再重新生成
compilation.emitAsset(
this.options.htmlFile,
new RawSource(newHtml)
)
}
)
})
}
}
module.exports = PreloadLCPImgWebpackPlugin
在webpack.config.js中production环境使用插件
const PreloadLCPImgWebpackPlugin = require('./webpack-plugins/preload-lcp-img-webpack-plugin')
// ...省略其他代码
if(isProduction) {
config.plugins.push(
new PreloadLCPImgWebpackPlugin(), // 预加载LCP图片
)
}
效果
优化后的LCP.png 没有LCP建议了.png
总结
webpack插件可以通过注册webpack的compiler、compilation实例的hook在特定的时机去【增加、删除、改变】内部的资源信息,最终可能影响构建输出。要写插件不难,难的是webapck里面几百个hook该用哪些hook,什么时机去做,能做什么事情,这个过程只能多参考其他插件的写法或者阅读源码。
网友评论