Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

前端资源加载失败优化 #25

Open
joeyguo opened this issue Jan 6, 2021 · 3 comments
Open

前端资源加载失败优化 #25

joeyguo opened this issue Jan 6, 2021 · 3 comments

Comments

@joeyguo
Copy link
Owner

joeyguo commented Jan 6, 2021

原文地址

Web 项目上线后,开始开门迎客,等待着来自大江南北、有着各式各样网络状态的用户莅临。在千差万别的网络状态中,访问页面难免会遇到前端资源加载失败的情况,占比或许不高,但一遇到,轻则页面样式错乱,重则白屏打不开,影响用户体验感受,紧急情况下甚至影响了用户的工作,属于非常严重的问题。本文将围绕着前端 JS 文件从如何监控加载失败、加载失败如何优化、始终加载失败又该如何处理等问题逐一分析。

如何监控资源加载失败

方案一:script onerror

我们可以给 script 标签添加上 onerror 属性,这样在加载失败时触发事件回调,从而捕捉到异常。

<script onerror="onError(this)"></script>  

并且,借助构建工具( 如 webpack 的 script-ext-html-webpack-plugin 插件) ,我们可以轻易地完成对所有 script 标签自动化注入 onerror 标签属性,不费吹灰之力。

new ScriptExtHtmlWebpackPlugin({
    custom: {
      test: /\.js$/,
      attribute: 'onerror',
      value: 'onError(this)'
    }
 })

方案二:window.addEventListener

上述方案已然不错,但我们也试想是否可以减少 onerrror 标签大量注入呢?类比脚本错误 onerror 的全局监控方式(详见:脚本错误量极致优化-监控上报与Script error),是否也可以通过 window.onerror 去全局监听加载失败呢?

答案否定的,因为 onerror 的事件并不会向上冒泡,window.onerror 接收不到加载失败的错误。冒泡虽不行,但捕获可以!我们可以通过捕获的方式全局监控加载失败的错误,虽然这也监控到了脚本错误,但通过 !(event instanceof ErrorEvent) 判断便可以筛选出加载失败的错误。

window.addEventListener('error', (event) => {
  if (!(event instanceof ErrorEvent)) {
    // todo
  }
}, true);

通过监控数据分析,我们发现现实情况不容乐观。访问页面时存在资源加载失败的情况超过了 10000 例/天,且随着页面访问量的上升而增加。

另外,监控资源加载失败的方式不止这些,上述两种方式都属于较好的方案,其他的方式就不再展开。

优化资源加载失败

方案一:加载失败时,刷新页面(reload)

有了监控数据后,便可着手优化。当资源加载失败时,刷新页面可能是最简单直接的尝试恢复方式。于是当监控到资源加载失败时,我们通过 location.reload(true) 强制浏览器刷新重新加载资源,并且为了防止出现一直刷新的情况,结合了 SessionStorage 限制自动刷新次数。

103794043-337f9500-507f-11eb-970d-a984e61252ad

通过监控数据发现,通过自动刷新页面,最终能恢复正常加载占异常总量 30%,优化比例不高,且刷新页面导致了出现多次的页面全白,用户体验不好。

方案二:针对加载失败的文件进行重加载

替换域名动态重加载

只对加载失败的文件进行重加载。并且,为了防止域名劫持等导致加载失败的原因,对加载失败文件采用替换域名的方式进行重加载。替换域名的方式可以采用重试多个 cdn 域名,并最终重试到页面主域名的静态服务器上(主域名被劫持的可能性小)

103795733-4e530900-5081-11eb-8e70-033ba7b492a6

然而,失败资源重加载成功后,页面原有的加载顺序可能发生变化,最终执行顺序发现变化也将导致执行异常。

103796632-6e36fc80-5082-11eb-86a4-c6cbf8d1b2ec

保证 JS 按顺序执行

在不需要考虑兼容性的情况下,资源加载失败时通过 document.write 写入新的 script 标签,可以阻塞后续 script 脚本的执行,直到新标签加载并执行完毕,从而保证原来的顺序。但它在IE、Edge却无法正常工作,满足不了我们项目的兼容性。

于是我们需要增加 “管理 JS 执行顺序” 的逻辑。使 JS 文件加载完成后,先检查所依赖的文件是否都加载完成,再执行业务逻辑。当存在加载失败时,则会等待文件加载完成后再执行,从而保证正常执行。

103797758-d5a17c00-5083-11eb-9b47-05fee718bf8d

手动管理模块文件之间的依赖和执行时机存在着较大的维护成本。而实际上现代的模块打包工具,如 webpack ,已经天然的处理好这个问题。通过分析构建后的代码可以发现,构建生成的代码不仅支持模块间的依赖管理,也支持了上述的等待加载完成后再统一执行的逻辑。

// 检查是否都加载完成,如是,则开始执行业务逻辑
function checkDeferredModules() {
    // ...
    if(fulfilled) {
        // 所有都加载,开始执行
        result = __webpack_require__(__webpack_require__.s = deferredModule[0]);
    }
}

103799361-f4a10d80-5085-11eb-81a2-5209ed0a0fdd

然而,在默认情况下,业务代码的执行不会判断配置的 external 模块是否存在。所以当 external 文件未加载完成或加载失败时,使用对应模块将会导致报错。

"react":  (function(module, exports) {
     eval("(function() { module.exports = window[\"React\"]; }());");
})

所以我们需要在业务逻辑执行前,保证所依赖的 external 都加载完成。最终通过开发 wait-external-webpack-plugin webpack 插件,在构建时分析所依赖的 external,并注入监控代码,等待所有依赖的文件都加载完成后再统一顺序执行。(详见:Webpack 打包后代码执行时机分析与优化

至此,针对加载失败资源重试的逻辑最终都通过构建工具自动完成,对开发者透明。重试后存在加载失败的情况优化了 99%。减少了大部分原先加载失败导致异常的情况。

始终加载失败该怎么办

用户网络千变万化,或临时断网、或浏览器突然异常,那些始终加载失败的情况,我们又该如何应对呢?
一个友好的提醒弹框或是最后的稻草,避免用户的无效等待,缓解用户感受。

103801420-ae00e280-5088-11eb-9e9d-ebcadad9ced5

总结

以上,便是对资源加载失败优化的整体方案,从如何监控加载失败、加载失败时重试、重试失败后的提醒等方面。大幅优化修正了加载失败的问题,也缓解着实遇到异常的用户使用体验。

如有不妥,恳请斧正,谢谢。

查看更多文章 >>
https://github.com/joeyguo/blog

@chenxiaoyao6228
Copy link

请教一下, 开发的时候如何模拟js加载失败的场景呢?

@joeyguo
Copy link
Owner Author

joeyguo commented Mar 11, 2021

@chenxiaoyao6228 可以用浏览器 devtool 的 block request(具体操作:打开 devtool -> network 菜单 -> 右键 js -> block request。或者通过抓包工具设置下 404 之类的。

@stackOverflowHidden
Copy link

!(event instanceof ErrorEvent)
这个可以筛选出资源加载失败类型的error吗?
new Error() instanceof ErrorEvent 返回的也是false

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants