SSR项目optimization.runtimeChunk还有哪些优化的可能
Madman 数据分析师

前言

webpack之optimization.runtimeChunk有什么作用?
runtimeChunk,直观翻译是运行时的chunk文件,其作用是啥呢,通过调研了解了一波,在此记录下。
何为运行时代码?
形如import(‘abc’).then(res=>{})这种异步加载的代码,在webpack中即为运行时代码。
在VueCli工程中常见的异步加载路由即为runtime代码。

1
2
3
4
5
6
7
8
9
{
path: '/about',
name: 'About',
// route level code-splitting
// this generates a separate chunk (about.[hash].js) for this route
// which is lazy-loaded when the route is visited.
component: () => import(/* webpackChunkName: "about" */ '../views/About.vue')
// component: About
}

optimization.runtimeChunk作用验证

实践才是真理,直接上测试。不过要是有时间仔细阅读理解webpack关于optimization.runtimeChunk的源码也是可以的。

1、搭建简单的vue项目,使用vuecli新建一个只需要router的项目,脚手架默认路由配置了一个异步加载的about路由,如上图所示

2、不设置runtimeChunk时,查看打包文件,此时不需要做任何操作,因为其默认是false,直接yarn build,此时生成的主代码文件的hash值为7d50fa23。

3、接着改变about.vue文件的内容,再次build,查看打包结果,发现app文件的hash值发生了变化。

设置runtimeChunk是将包含chunks 映射关系的 list单独从 app.js里提取出来,因为每一个 chunk 的 id 基本都是基于内容 hash 出来的,所以每次改动都会影响它,如果不将它提取出来的话,等于app.js每次都会改变。缓存就失效了。设置runtimeChunk之后,webpack就会生成一个个runtime~xxx.js的文件。
然后每次更改所谓的运行时代码文件时,打包构建时app.js的hash值是不会改变的。如果每次项目更新都会更改app.js的hash值,那么用户端浏览器每次都需要重新加载变化的app.js,如果项目大切优化分包没做好的话会导致第一次加载很耗时,导致用户体验变差。现在设置了runtimeChunk,就解决了这样的问题。所以这样做的目的是避免文件的频繁变更导致浏览器缓存失效,所以其是更好的利用缓存。提升用户体验。具体关于浏览器缓存参考:https://web.dev/use-long-term-caching/

4、新建vue.config.js,配置runtimeChunk,第一次打包,然后修改about,在打包一次,查看2次打包之后app文件的hash值的变化。

1
2
3
4
5
6
7
8
9
// vue.config.client.js
module.exports = {
productionSourceMap: false,
configureWebpack: {
runtimeChunk:{
name: 'manifest'
}
}
}

设置了runtimeChunk配置后,就算对应页面组件内容变更了,打包的app.xxxxxxxx.js的哈希版本号不会变更。会报表抽离生成一个manifest.xxxxxxx.js的文件。

设置runtimeChunk导致的新问题

1、查看一下manifest.xxxxxxx.js文件内容:

发现文件很小,且就是加载chunk的依赖关系的文件。虽然每次构建后app的hash没有改变,但是manifest.xxxxxxx.js会变啊。每次重新构建上线后,浏览器每次都需要重新请求它,它的 http 耗时远大于它的执行时间了,所以建议不要将它单独拆包,而是将它内联到我们的 index.html 之中。

最终解决方案

SPA解决方案

对于单页面应用(SPA)可以webpack的html相关插件。这边我们使用script-ext-html-webpack-plugin来实现。(也可使用html-webpack-inline-source-plugin,其不会删除manifest.xxxxxxx.js文件。)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// vue.config.js
const ScriptExtHtmlWebpackPlugin = require('script-ext-html-webpack-plugin')
module.exports = {
productionSourceMap: false,
configureWebpack: {
optimization: {
runtimeChunk:{
name: 'manifest'
}
},
plugins: [
new ScriptExtHtmlWebpackPlugin({
inline: /manifest.+\.js$/ //正则匹配runtime文件名
})
]
},
chainWebpack: config => {
config.plugin('preload')
.tap(args => {
args[0].fileBlacklist.push(/manifest.+\.js$/) //正则匹配runtime文件名,去除该文件的preload
return args
})
}
}

重新打包,查看index.html文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<!DOCTYPE html>
<html lang=en>

<head>
<meta charset=utf-8>
<meta http-equiv=X-UA-Compatible content="IE=edge">
<meta name=viewport content="width=device-width,initial-scale=1">
<link rel=icon href=/favicon.ico>
<title>runtime-chunk</title>
<link href=/js/about.cccc71df.js rel=prefetch>
<link href=/css/app.b087a504.css rel=preload as=style>
<link href=/js/app.9f1ba6f7.js rel=preload as=script>
<link href=/css/app.b087a504.css rel=stylesheet>
</head>

<body><noscript><strong>We're sorry but runtime-chunk doesn't work properly without JavaScript enabled. Please enable it
to continue.</strong></noscript>
<div id=app></div>
<script>(function (e) { function r(r) { for (var n, a, i = r[0], c = r[1], l = r[2], f = 0, s = []; f < i.length; f++)a = i[f], Object.prototype.hasOwnProperty.call(o, a) && o[a] && s.push(o[a][0]), o[a] = 0; for (n in c) Object.prototype.hasOwnProperty.call(c, n) && (e[n] = c[n]); p && p(r); while (s.length) s.shift()(); return u.push.apply(u, l || []), t() } function t() { for (var e, r = 0; r < u.length; r++) { for (var t = u[r], n = !0, a = 1; a < t.length; a++) { var c = t[a]; 0 !== o[c] && (n = !1) } n && (u.splice(r--, 1), e = i(i.s = t[0])) } return e } var n = {}, o = { "runtime~app": 0 }, u = []; function a(e) { return i.p + "js/" + ({ about: "about" }[e] || e) + "." + { about: "cccc71df" }[e] + ".js" } function i(r) { if (n[r]) return n[r].exports; var t = n[r] = { i: r, l: !1, exports: {} }; return e[r].call(t.exports, t, t.exports, i), t.l = !0, t.exports } i.e = function (e) { var r = [], t = o[e]; if (0 !== t) if (t) r.push(t[2]); else { var n = new Promise((function (r, n) { t = o[e] = [r, n] })); r.push(t[2] = n); var u, c = document.createElement("script"); c.charset = "utf-8", c.timeout = 120, i.nc && c.setAttribute("nonce", i.nc), c.src = a(e); var l = new Error; u = function (r) { c.onerror = c.onload = null, clearTimeout(f); var t = o[e]; if (0 !== t) { if (t) { var n = r && ("load" === r.type ? "missing" : r.type), u = r && r.target && r.target.src; l.message = "Loading chunk " + e + " failed.\n(" + n + ": " + u + ")", l.name = "ChunkLoadError", l.type = n, l.request = u, t[1](l) } o[e] = void 0 } }; var f = setTimeout((function () { u({ type: "timeout", target: c }) }), 12e4); c.onerror = c.onload = u, document.head.appendChild(c) } return Promise.all(r) }, i.m = e, i.c = n, i.d = function (e, r, t) { i.o(e, r) || Object.defineProperty(e, r, { enumerable: !0, get: t }) }, i.r = function (e) { "undefined" !== typeof Symbol && Symbol.toStringTag && Object.defineProperty(e, Symbol.toStringTag, { value: "Module" }), Object.defineProperty(e, "__esModule", { value: !0 }) }, i.t = function (e, r) { if (1 & r && (e = i(e)), 8 & r) return e; if (4 & r && "object" === typeof e && e && e.__esModule) return e; var t = Object.create(null); if (i.r(t), Object.defineProperty(t, "default", { enumerable: !0, value: e }), 2 & r && "string" != typeof e) for (var n in e) i.d(t, n, function (r) { return e[r] }.bind(null, n)); return t }, i.n = function (e) { var r = e && e.__esModule ? function () { return e["default"] } : function () { return e }; return i.d(r, "a", r), r }, i.o = function (e, r) { return Object.prototype.hasOwnProperty.call(e, r) }, i.p = "/", i.oe = function (e) { throw console.error(e), e }; var c = window["webpackJsonp"] = window["webpackJsonp"] || [], l = c.push.bind(c); c.push = r, c = c.slice(); for (var f = 0; f < c.length; f++)r(c[f]); var p = l; t() })([]);</script>
<script src=/js/chunk-vendors.1e5c55d3.js></script>
<script src=/js/app.9f1ba6f7.js></script>
</body>
</html>

index.html中已经没有对manifest.xxxxxxx.js的引用了,而是直接将其代码写入到了index.html中,故不会在请求文件,减少http请求。

SSR解决方案

webpack打包插件对于服务端渲染的html没有用,因为服务端的js引用关系都是根据 vue-ssr-server-bundle.json 动态注入的。所以我就没有从webpack插件入手,直接从Node服务端去调整,不动webpack打包配置。

1
2
3
4
5
6
7
8
const serverBundle = require('../public/vue-ssr-server-bundle.json')
const clientManifest = require('../public/vue-ssr-client-manifest.json')
clientManifest.initial = clientManifest.initial.filter(path => {
const isManifestPath = /manifest.+\.js$/.test(path)
if (isManifestPath) manifestPath = path
return !isManifestPath
})
vueRenderer = createRenderer(serverBundle, { template, clientManifest })

在createRenderer之前把从vue-ssr-client-manifest.json获取到的initial文件路径中把manifest.xxxxxxx.js文件筛选出来并从initial中移除(移除后在自动注入引用link的时候就不会把manifest文件用link标签引入),并记录manifest.xxxxxxx.js的具体值,因为这个manifestPath后面我们还要用到,后面需要读取这个文件路径里面的js代码注入到渲染的html里面去。

1
2
3
4
5
6
7
8
const exportManifestJsCode = () => {
let script = ''
if (manifestPath) {
const manifestJsCode = fs.readFileSync(resolve(`../public/${manifestPath}`), 'utf-8')
script = `<script>${manifestJsCode}</script>`
}
return script
}

获取manifestPath里面的代码,生成script代码段。

1
2
3
4
5
6
context.manifestJsCode = exportManifestJsCode()
vueRenderer.renderToString(context, (err, html) => {
if (err) {
return handleError(err, req, res)
}
res.send(html)

把生成的ManifestJsCode导出给context.manifestJsCode

1
2
3
4
5
6
7
8
9
10
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
</head>
<body>
{{{manifestJsCode}}}
<!--vue-ssr-outlet-->
</body>
</html>

最后在ssr的html模版对应位置用3大括号{{{manifestJsCode}}}将manifestJsCode注入到html对应位置。

⚠️这个是对VueSSR指南手动构建项目的解决方案,Nuxt服务端渲染框架解决办法还没研究。

  • 本文标题:SSR项目optimization.runtimeChunk还有哪些优化的可能
  • 本文作者:Madman
  • 创建时间:2022-09-21 11:45:17
  • 本文链接:https://www.patpat.site/开发/前端/SSR项目optimization-runtimeChunk还有哪些优化的可能.html
  • 版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
 评论