- Published on
基于 Webpack 实现文件预取
序言
日常开发中,我们经常会遇到一些性能优化的问题。其中,文件预取是一个非常重要的优化手段。在进行相应的方案设计时,我们需要对当前项目结构及组成有相应的认知,以便选择合适的优化策略。
什么是文件预取
文件预取是指在浏览器空闲时,提前加载下一个页面所需的资源。这样可以提高页面加载速度,提升用户体验。
基于 Webpack 项目实现文件预取
由于该优化项目是通过 webpack 进行构建的,所以我们一切的优化都需要基于 webpack 来进行。
在获取到文件预取需要我们首先获取到文件列表,然后对文件列表进行遍历以使用。
webpack-assets-manifest
(webpack-assets-manifest)[https://github.com/webdeveric/webpack-assets-manifest/blob/master/src/WebpackAssetsManifest.js] 可以直接获取到 webpack 的所有构建物,简单使用方式如下:
const path = require('path')
const WebpackAssetsManifest = require('webpack-assets-manifest')
module.exports = {
entry: {
// Your entry points
},
output: {
path: path.join(__dirname, 'dist'),
filename: '[name]-[hash].js',
chunkFilename: '[id]-[chunkhash].js',
},
module: {
// Your loader rules go here.
},
plugins: [
new WebpackAssetsManifest({
// Options go here
}),
],
}
构建完成后可获取到的产出物类型如下:
type RouteManifest = Record<string, string>
webpack-route-manifest
(webpack-route-manifest)https://github.com/lukeed/webpack-route-manifest 可以基于路由返回 webpack 构建物列表,简单使用方式如下:
// webpack.config.js
const RouteManifest = require('webpack-route-manifest')
module.exports = {
// ...
plugins: [
new RouteManifest({
routes(str) {
// Assume all entries are '../../../pages/Home' format
let out = str.replace('../../../pages', '').toLowerCase()
if (out === '/article') return '/blog/:title'
if (out === '/home') return '/'
return out
},
}),
],
}
构建完成后可获取到的产出物类型如下:
type RouteManifest = Record<
string,
{
type: 'script' | 'style'
href: string
}[]
>
webpack optimization runtimeChunk
在 webpack 文档中,提供了 (分离 manifest)[https://survivejs.com/webpack/optimizing/separating-runtime/] 的文章,该文章初始对我产生了一定的误解,期望通过读取分离的 manifest 文件后对内容进行分析进行预取。
但在实际操作后,发现该文件返回的是一个 .js 文件,且无法在当次构建时读取该文件。
prefetch
prefetch & preload
prefetch 和 preload 的区别在于,preload 会在当前页面加载时立即加载资源,而 prefetch 会在浏览器空闲时加载资源。所以根据当前场景,使用 prefetch 更合适
方案 1 prefetch 所有文件
prefetch 部分的代码就相对简单,我们已经获取到了文件列表,遍历插入即可
import axios from 'axios'
import { delay } from './util'
const getAssetManifest = () => {
return axios.get < Record < string, string >> `/asset-manifest.json?t=${Date.now()}`
}
const DELAY_TIME = 15e3
export const prefetchPath = async () => {
/** 等待 DELAY_TIME 后再开始执行,错峰执行 */
Promise.all([getAssetManifest(), delay(DELAY_TIME)]).then(([res]) => {
Object.values(res.data).forEach((file) => {
const link = document.createElement('link')
link.rel = 'prefetch'
// 判断文件后缀,css, js 提前加载
if (file.endsWith('.css') || file.endsWith('.js')) {
link.href = pathPrefix + file
document.head.appendChild(link)
}
})
})
}
方案 2 在链接 hover 时进行 prefetch
import axios from 'axios'
import rmanifest, { FileMap } from 'route-manifest'
// 可以考虑存储于 localStorage
let localRouterManifest: FileMap | null = null
const getRouterManifest = (): Promise<FileMap> => {
if (localRouterManifest) {
return Promise.resolve(localRouterManifest)
}
return axios.get(`/route-manifest.json?t=${Date.now()}`).then((res) => {
localRouterManifest = res.data
return res.data
})
}
const getPathFiles = async (path: string) => {
const routerManifest = await getRouterManifest()
return ['/index.js', '/index.jsx', '/index.tsx', '.js', '.jsx', '.tsx']
.map((suffix) => path + suffix)
.flatMap((filePath) => rmanifest(routerManifest, filePath).files)
.map((i) => ({
type: i.type,
href: pathPrefix + i.href,
}))
}
const toPrefetch = new Set()
/**
* 基于 [webpack-route-manifest](https://github.com/lukeed/webpack-route-manifest) 及 [quickLink](https://github.com/GoogleChromeLabs/quicklink) 实现的路由文件预取
*/
export const prefetchPath = async (path: string) => {
const files = await getPathFiles(path)
files.forEach((file) => {
if (toPrefetch.has(file.href)) {
return
}
toPrefetch.add(file.href)
const link = document.createElement('link')
link.rel = 'preload'
link.href = file.href
if (file.type === 'script') {
link.as = 'script'
} else if (file.type === 'style') {
link.as = 'style'
}
// append the link tag to the head of the document
document.head.appendChild(link)
})
}
// 为 a 标签添加 hover 后执行 prefetchPath 的代码
方案对比
- 方案 1 适用于所有文件的预取,但可能会导致一些不必要的资源浪费
- 方案 2 适用于路由文件的预取,但对于部分没有路由的文件无法进行预取,例如通过
import()
异步加载的文件