基于vue2的h5项目解决iOS Safari及其WebView下载脚本时阻塞页面显示导致白屏时间过长问题
[ 2023/09/17, Vue , 649阅, 0评 ]

一个基于vue2的h5项目,被嵌套在安卓APP和iOS APP中使用。因为某些原因无法在webview中使用缓存,也无法使用cdn方式。

在经过分包、按需加载、代码清理、压缩资源文件、减少并发请求等常规手段优化后,网络较差的情况下较长的白屏时间仍然很尴尬。于是在index.html中加上了默认loading动画效果,这样用户在加载完入口文件后就可以直接看到动画,待vue初始化完成时再关闭loading动画,体验更佳。

辣么问题来了,在PC和Android设备上进行测试,都能达到预期效果。但是在iOS设备上,不管是 Safari 还是基于 iOS的WebView中,index.html的动画都无法显示,仍然是较长时间的白屏,然后直接出现了项目内部的动画。

查阅相关资料,在某乎的一篇文章(详见文末的“相关资料”)中有如下结论:

当前所有版本的 iOS Safari(包括所有基于 iOS WebView 的浏览器)都存在一个 bug,下载中的脚本会阻塞页面的显示,无论脚本是否在页面底部或是否有 defer 或 async 属性。

如果动态创建 script 标签并在异步回调中进行脚本加载,可以避免阻塞,大幅度改善 Mobile Safari 的首屏渲染时间,经统计可能会快 240 毫秒以上。

当前的的最佳实践是,在 rAF 后再推迟以进行脚本加载。

经过验证确实如此,于是有了以下相对perfect的方案:

index.html文件:

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title><%= htmlWebpackPlugin.options.title %></title>
<!-- 默认loading样式 -->
<style>
#app,body,html{width:100%;height:100%;padding:0;margin:0}
.app-loading{position:fixed;z-index:999999999;top:0;left:0;display:flex;flex-direction:column;align-items:center;justify-content:center;width:100%;height:100%;}
.app-loading .loading-wrap{display:flex;align-items:center;justify-content:center;padding:98px}
.app-loading .dot{position:relative;box-sizing:border-box;display:inline-block;width:32px;height:32px;font-size:32px;transform:rotate(45deg);animation:app-loading-rotate 1.2s infinite linear}
.app-loading .dot i{position:absolute;display:block;width:14px;height:14px;background-color:#409eff;border-radius:100%;opacity:.3;transform:scale(.75);transform-origin:50% 50%;animation:app-loading-spin-move 1s infinite linear alternate}
.app-loading .dot i:nth-child(1){top:0;left:0}
.app-loading .dot i:nth-child(2){top:0;right:0;animation-delay:.4s}
.app-loading .dot i:nth-child(3){right:0;bottom:0;animation-delay:.8s}
.app-loading .dot i:nth-child(4){bottom:0;left:0;animation-delay:1.2s}
@keyframes app-loading-rotate{to{transform:rotate(405deg)}}
@keyframes app-loading-spin-move{to{opacity:1}}
</style>
<!-- 打包时注入 -->
<% if (htmlWebpackPlugin.options.isBuild) { %>
  <% for (var i in htmlWebpackPlugin.files.css) { %>
    <link href="<%= htmlWebpackPlugin.files.css[i] %>" rel="stylesheet">
  <% } %>
  <% for (var i in htmlWebpackPlugin.files.js) { %>
    <link href="<%= htmlWebpackPlugin.files.js[i] %>" rel="preload" as="script" />
  <% } %>
<% } %>
</head>

<body>
<noscript>
  <strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<!-- 默认loading -->
<div class="app-loading">
  <div class="loading-wrap">
    <span class="dot dot-spin"><i></i><i></i><i></i><i></i></span>
  </div>
</div>
<!-- 打包时注入 -->
<% if (htmlWebpackPlugin.options.isBuild) { %>
<script>
{
  const loadScript = () => {
    let jsArr = '<%= htmlWebpackPlugin.files.js %>'.split(',');
    for(let item of jsArr) {
      let script = document.createElement('script')
      script.src = item
      document.body.appendChild(script)
    }
  }
  // 使用一个非 microtask 回调,使回调执行在下一个 event loop 中
  const scheduleNextTick = callback => {
    const el = new Image()
    el.src = 'data:,'
    el.onload = el.onerror = callback
  }
  const isPageVisible = (document.visibilityState || document.webkitVisibilityState) === 'visible';
  if (isPageVisible) {
    // raf 后推迟,不在 rendering 阶段执行可能阻塞的操作
    requestAnimationFrame(() => scheduleNextTick(loadScript))
  } else {
    loadScript()
  }
}
</script>
<% } %>
</body>
</html>

vue.config.js中:

module.exports = {
  // ...
  chainWebpack(config) {
    // 移除 preload 插件
    config.plugins.delete('preload')
    // 移除 prefetch 插件
    config.plugins.delete('prefetch')
    // 当打包时才执行
    config.when(process.env.NODE_ENV === 'production', (config) => {
      config.plugin('html').tap((args) => {
        // 关闭自动注入
        args[0].inject = false
        // 自定义一个开启手动注入的标识
        args[0].isBuild = true
        return args
      })
    })
  },
  // ...
}

在vue-router的全局后置钩子中关闭loading

router.afterEach((to, from) => {
  document.querySelector('.app-loading').style.display = 'none'
})

相关资料

有朋自远方来...评论一下呗O(∩_∩)O