记一次简单的页面加载优化
前言
一次系统内子模块功能的 i18n
功能上线部署后,页面发生了明显的加载缓慢,接口阻塞的情况。该功能主要由一个主页面引用了 20 个左右的按需使用的子页面进行远程加载并读取渲染到主页面中。
在本次更新后,发现所有子页面的读取速度明显变慢,且由于需要读取页面内容加载为 DOM 渲染到主页面中,JS 的执行导致主页面的接口读取也被阻塞
分析
通过 DevTools 中的请求分析得知,在加载子页面时,每个子页面的读取速度明显变慢,且在读取页面内容后需要将页面转为 DOM 内容追加到主页面,至此可得知所有子页面都具有相同的问题,而主页面加载无明显的长任务(渲染耗时超出1000ms)。
并且存在子页面重复读取i18n
文件的行为
再通过堆栈调用,发现长任务发生在国际化的渲染中,平均每个子页面的国际化读取都耗时 1-2 秒!
再继续追踪调用堆栈,最终确定核心问题代码发生于这个代码:
querySelectorAll("[data-xx").forEach((el) => {
el.innerText = t(el.dataset.xx);
});
执行流程
该代码执行时,其页面内容和脚本其实已经均已挂载到了主页面中了。
那么该代码执行时,上下文其实已经在主页面中了,那么就会导致本身只需要读取子页面中的 DOM 元素变为读取主页面所有内容 + 已加载子页面的 DOM 元素
INFO
所以要解决这个问题,最终我们要将 i18n 的读取范围边界进行控制,且将 i18n 读取结果进行缓存和版本控制,避免重复读取以及增加读取速度。
解决
控制边界
将子页面最外层或者主页面中的读取逻辑中,增加最终渲染结果 DOM 的嵌套,指定 ID 或者类名 控制边界,修改代码为:
const parent = querySelector("#xxx");
parent.querySelectorAll("[data-xx").forEach((el) => {
el.innerText = t(el.dataset.xx);
});
缓存读取
将 i18n 在主页面读取后缓存到本地, 子页面移除所有读取国际化文件并整合到主页面的国际化文件中,通过缓存优化加载速度
// Main
fetch(xxxx)
.then((res) => res.json())
.then((res) => {
localstorage.setItem(
"i18n",
JSON.stringify({
version: "1.0.0",
map: i18Map,
})
);
});
// Child
let i18nMap = localstorage.getItem("i18n");
if (i18nMap) {
i18nMap = JSON.parse(i18nMap);
scopeI18n.map = i18nMap.map; //
scopeI18n.version = i18nMap.version;
// 比较版本 ...
// 渲染逻辑 ...
} else {
// 无缓存,属于非引用页面加载,手动读取一次i18n文件或者加载默认文本...
}
效果
至此除了主页面的脚本执行, 所有子页面加载均已恢复正常
从
956毫秒
提升到45.7
毫秒,近 20 倍的提升,并将所有子页面重复读取国际化文件请求移除, 此时页面渲染速度变回正常。 并且子页面也不需要在进行国际化文件读取。