前言
我想写一个系列:前端性能优化实践方案。网上虽然一搜一大把这样的文章,但大多缺乏体系化。也有很多讲性能优化的书籍,但其实想照着书上的知识进行实践,还是挺难的一件事。
这是该系列的第一篇文章
由于自己的知识水平有限,文章中难免有出错的地方,如果你看到,望指正。
由于刚开始写作,文章风格和写作方式还比较青涩,如果你有任何建议,可以直接向我反馈,感谢Thanks♪(・ω・)ノ
背景
最近在做页面性能优化相关工作,发现项目中很多图片尺寸都比较大(>1M),虽然使用了webp图片方案,但对于不支持webp图片格式的设备来说,图片资源加载慢的问题并没有得到有效解决。针对这个问题,有两种解决方案,一是让设计同学重新给小尺寸的图;二是自己压缩图片。方案一治标不治本,且对不同的项目无法实现复用,故这里采用第二种方案。
压缩图片采用的是 https://tinypng.com/ 方式进行压缩,它单次最大支持20张图片的压缩个数,如果项目图片比较少,可以直接进行手动压缩;由于我优化的项目中图片个数达到了上百张,因此,写了webpack插件来提升开发效率,同时解决了单次压缩个数的限制。
具体实现
在讲具体实现之前,我这里先说一下自己的实现思路,我会按照实现思路依次讲解实现步骤【需要了解webpack插件实现原理】:
1.获取项目构建中的图片资源
2.将获取到的图片资源依次上传到 https://tinypng.com/ 官网进行压缩
3.获取压缩后的图片资源,替换原有图片
1.获取项目构建的图片资源
// 兼容不同版本的webpack
apply(compiler) {
// webpack verison >= 4
if (compiler.hooks && this._options.compress) {
const isWebpack4 = compiler.webpack ? false : typeof compiler.resolvers !== 'undefined';
if (isWebpack4) {
compiler.hooks.emit.tapPromise(pluginName, compilation => {
// handleImgAssets 方法具体处理图片资源
return Promise.resolve(this.handleImgAssets(compilation))
})
} else { // webpack 5
compiler.hooks.compilation.tap(pluginName, compilation => {
compilation.hooks.processAssets.tapPromise(pluginName, () => {
return Promise.resolve(this.handleImgAssets(compilation))
})
})
}
} else if (compiler.plugin && this._options.compress) {
// webpack3的写法
compiler.plugin('emit', async (compilation, callback) => {
await this.handleImgAssets(compilation)
callback()
})
} else {
console.log(`The webpack version number supported by ${pluginName} is 3-5!, install: https://webpack.js.org/`)
}
}
通过上面的代码,我们就可以拿到compilation对象,图片资源就在这个对象里面,我们把它作为参数传给handleImgAssets方法,具体处理细节在handleImgAssets中。接下来,让我们看看它具体干了些啥。
2.将获取到的图片资源依次上传到 https://tinypng.com/ 官网进行压缩
async handleImgAssets(compilation) {
// 通过 assets 字段获取所有的静态资源
const ImgAssets = compilation.assets
// 过滤出图片
let images = Object.keys(compilation.assets).filter(asset => IMG_TEST.test(asset))
// 如果图片不存在,直接返回
if (!images.length) {
return Promise.resolve()
}
// 插件支持自定义压缩图片的尺寸区间,这里就是找出真正需要压缩的图片
// 比如我设置了 大于 10kb的图片才进行压缩,这里会过滤出尺寸大于10kb的图
images = this.filterImages(images, ImgAssets)
// 我们将每个图片压缩过程转成promise
// 具体实现在 compressImg 方法里
const imgPromises = images.map(img => this.compressImg(ImgAssets, img))
const spinner = Ora('Compressing Image......').start()
// 并发控制
await this.promiseLimit(imgPromises, this._options.concurrency).then(res => {
spinner.stop()
this._options.log && res && res.forEach(msg => console.log(msg))
})
}
3.获取压缩后的图片资源,替换原有图片
/** 处理图片压缩工作 */
compressImg(assets, key) {
try {
// 获取图片的原始资源
const file = assets[key].source()
// 上传图片资源,进行压缩。具体实现在uploadImg方法里
const originData = await this.uploadImg(file)
// 获取压缩后的图片资源
const compressedData = await this.downloadImg(originData.output.url)
// 替换原资源
assets[key] = new RawSource(Buffer.alloc(compressedData.length, compressedData, 'binary'))
return new Promise((resolve, reject) => resolve(msg))
} catch (error) {
// do something
}
}
/**上传图片方法,接口会自动进行压缩 */
uploadImg(source) {
const header = DefaultHeader()
return new Promise((resolve, reject) => {
const req = Https.request(header, res => res.on('data', data => {
const resObj = JSON.parse(data.toString())
// 如果报错,直接reject
// 如果成功,resolve 接口返回的对象,里面包含压缩后的图片地址,需要再次请求去下载
resObj.error ? reject(resObj.message) : resolve(resObj)
}))
req.write(source, 'binary')
req.on('error', e => reject(e))
req.end()
})
}
/** 下载压缩后的图片 */
downloadImg(url) {
// 这里就是拿到刚刚接口返回的地址,再次请求,下载图片资源就好
const URL = new Url.URL(url)
return new Promise((resolve, reject) => {
const req = Https.request(URL, res => {
let file = ''
res.setEncoding('binary')
res.on('data', data => file += data)
res.on('end', () => resolve(file))
})
req.on('error', e => reject(e))
req.end()
})
}
以上就完成了图片压缩工作,完整的代码可查看这里。该插件在项目打包的过程中,实现图片压缩功能,当然,如果压缩后的效果设计同学不满意,我们去掉压缩即可,不会对原有图片资源造成污染。
我将上述插件发布成了npm包,想使用的同学可以直接安装使用。目前公司内部很多项目都在使用,整体图片资源大小降低了40%以上,页面加载速度和用户体验也得到了明显的提升。
结尾
目前只支持线上压缩功能,如果你的工作环境没有网,那该插件不能正常工作。后期想通过canvas来实现离线压缩,感兴趣的同学可以一起搞一下~ヾ(◍°∇°◍)ノ゙【你将会成为共同作者】