技术分享 | 浏览器预加载资源技术的应用实践

独家号 极客精选 原文链接

源宝导读:Web技术日新月异,浏览器也越来越强大,使得开发者能够一次次突破技术的极限,为用户提供更好的使用体验。本文将分享天际DevOps平台如何利用浏览器新特性提升网页性能的技术实践。

一、背景

    天际-DevOps平台(简称 RDC)是基于 VUE 技术栈的单网页应用(SPA),所有界面都是由 JavaScript 程序动态绘制而成。所以充分优化前端性能,提升资源(主要是 CSS 和 JS )的加载速度,特别是优化页面首次打开的资源加载速度显得尤为重要。只有 JS 资源文件加载速度越快,浏览器才能越快的解析 JavaScript 脚本,从而更加快速的绘制出系统界面,呈现给用户。

    提到前端性能优化,我们通常会想到压缩资源文件大小,或者启用浏览器缓存,可以起到较少 HTTP 请求,优化资源加载速度的效果,但这些手段主要提升重复访问相同资源时的加载速度。默认情况下,浏览器只会先加载 HTML 中声明的资源。如果没有声明,浏览器是不会提前加载资源的。那有没有什么办法能提前加载页面所需资源,优化首次的加载速度呢?

    很幸运,随着 Web 技术的发展,现代的浏览器可以做到提前加载页面所需资源了。使用资源提示伪指令:prefetch 和 preload(https://www.w3.org/TR/resource-hints/),可以提前告知浏览器加载资源,从而可以缩短网站的(首次)加载速度,优化页面性能。

二、什么是<link rel =“prefetch”>?

    prefetch (预取用),它可以利用浏览器的空闲时间来预取用(下载)用户可能在不久的将来会访问的资源。换句话说,浏览器将提前加载用户将来可能要访问的页面资源。浏览器将这些提前下载的资源存储在本地缓存中,以便在用户最终访问该页面的资源时能更快地发送请求的信息,并非常快速的加载资源所以,使用 prefetch 技术,不会减少 HTTP 请求,但会提升使用资源时的资源加载速度。

2.1、Prefetch 使用场景和注意事项

    prefetch 预取用的资源在加载完成后,并不会像加载普通的资源那样,加载完成后浏览器不会马上解析资源,而只是缓存到本地。不立即执行解析,特别是不立即解析脚本文件,就意味着不会阻塞页面加载其它资源。只有用户访问了调用这些资源的 页面的时候,才会立刻解析。也就是说,prefetch 的使用场景是用来提前加载那些用户将来会访问的页面资源,优化将来会访问页面资源的首次加载速度。

    具体的场景例如:

  • 当一系列页面很像幻灯片时,请加载接下来的1-3页,之前的1-3页(假设它们不占很大)。

  • 加载要在网站上大多数页面上使用的图像。

  • 将搜索结果的下一页加载到您的网站上。

    当然,使用 prefetch 预取用技术也有需要注意的事项。那就是当用户始终都没有访问 prefetch 预期在将来可能访问的页面资源时,使用 prefetch 就可能会花费额外的带宽(流量)。

2.2、Prefetch 浏览器支持情况

    浏览器对 prefetch 的支持情况还是不错的,除了  Safari 浏览器目前还不支持,其余的主流浏览器都已经支持了,甚至连 IE11 都支持了。而根据我们接入天眼系统(天际大家庭的新成员,天眼(FAST),是统一的用户行为&应用性能&应用异常数据的采集、分析、监控平台)的统计,目前使用 safari 浏览器访问的用户比例只占到全体用户的0.54%,因此使用 prefetch 技术给使用 DevOps 平台的绝大多数用户带来性能提升。而且在不支持 prefetch 的浏览器中,浏览器只会忽略它,不会带来额外的影响。遵循了渐进式用户体验提升的原则。

2.3、Prefetch 资源如何选择?

    前文提到了,使用 prefetch 用来加载稍后会用到的资源,如果用户没有访问期望的页面,使用 prefetch 就可能会花费额外的带宽。那么我们应该怎么选择要加载的资源,避免额外消耗呢?

    首先,我们需要知道,浏览器对 HTML 页面中调用的各种资源是有着不同级别的优先级(Priorty )区分的。在 Chrome 浏览器中,我们可以按 F12 键,打开 DevTools 工具面板,然后点击 Network 标签,接着右键点击 Name 标题栏,在弹出的菜单中选择 Priorty 选项(默认是不展示的),这样就可以看到浏览器的资源优先级的列数据了。

    如图所示,我们可以看到优先级高的资源(显示为 Highest 或者 High 的资源)基本都是 CSS、JS和字体资源。那么我们使用 prefetch 加载资源也应该选择这些优先级高的资源。应该使用 prefetch 加载那些用户使用比较频繁的模块资源,这样用户接下来大概率会使用到这些资源,从而避免 prefetch 加载的资源用户没有使用,而造成额外的带宽消耗。

三、什么是<link rel =“preload”>?

    preload (预加载),它告诉浏览器如何将特定资源提前提取到当前页面中。本质上,它会在当前页面开始加载之前在浏览器后台提前下载资源。并且,浏览器通常以中等优先级,而不是布局阻塞的方式来获取此资源。使用 preload 提前加载的资源,不会花费额外的带宽。也就是不会产生额外的 HTTP 请求,这个是 preload 与 prefetch 不同的地方之一。

3.1、Preload 使用场景和注意事项

    preload 和 prefetch 类似,使用 preload 加载的资源在加载完成后浏览器也不会立刻解析。preload 预加载加载资源的使用场景是 preload 预加载当前访问页面会立刻使用到的资源。虽然使用 preload 提前加载的资源,不会花费额外的带宽,但如果 preload 预加载的资源,在加载完成后3秒钟后还未被使用,这时(Chrome)浏览器在控制台中会显示警告,提示预加载的资源在当前页面没有被引用。

3.2、Preload 浏览器支持情况

    preload 比 prefetch 的 Web 标准更新(https://w3c.github.io/preload/)来得更晚,但可以看到,浏览器对 preload 的支持也是不错的,目前主流浏览器只有 IE11 不支持。而根据天眼的统计,DevOps 平台目前只有 1.11% 的用户使用 IE11 浏览器访问。同 prefetch 的情况一样,在不支持的浏览器中,也是直接忽略 preload 的,也不会产生任何负面的影响。也遵循了渐进式用户体验提升的原则。

3.3、Preload 资源如何选择?

    preload 资源的选择规则就简单很多, prload 预加载当前页面就要用到的资源。当然,也是选择优先级高的资源。使用过 lighthouse  前端性能分析工具的同学应该都知道,在 lighthouse  性能优化指导规则中就有一项是“预加载关键请求”。指的就是使用 preload 预加载当前页面的优先级高(关键)的资源。

3.4、使用 Preload 可以提升多少性能?

    没有使用 preload 加载资源前,浏览器仅在下载,解析和执行 app.js 后才开始加载它之后的 2 个资源。

   无预载链接,styles.css并且ui.js只要求后,app.js已被下载,解析和执行。

    但是我们知道 ui.js 和 style.css 这些资源很重要,应该尽快下载。

    带有预加载链接,styles.css、ui.js 和 app.js 同时请求

    preload 潜在的性能提升是基于声明了预加载链接,浏览器将能够在多早之前启动资源请求。例如,如果 app.js 花费 200 毫秒来下载,解析和执行,则每个资源的潜在节省为200毫秒,因为 app.js 它不再是每个请求的瓶颈,在加载 app.js  的时候不会阻塞其它资源的下载。

四、Preload 和 Prefetch 该怎么使用?

    在了解完 preload 和 prefetch 功能、作用,以及注意事项之后,接下来就需要看看到底怎么使用了?

4.1、Preload 和 Prefetch 的调用方式

    preload 和 prefetch 的使用方式一样,采用 <link rel="prefetch"> 或者 <link rel="preload"> 方式加载资源文件,废话不多说,直接上代码:

<head>    <meta charset="utf-8">    <title> JS and CSS preload example </title> <!-- 在 header 区域加入 -->    <link rel="preload" href="style.css" as="style">    <link rel="preload" href="main.js" as="script">    <link rel="prefetch" href="news.js" as="script">    <link rel="stylesheet" href="style.css"></head>
<body> <h1> bouncing balls </h1> <canvas></canvas> <script src="main.js" defer></script></body>

    另外,还需要指定:可以看到,prefetch 和 preload 调用方式很简单。使用 <link> 标签,设置 rel 属性设置值为 preload 或者 prefetch<link> 标签成为我们想要的任何资源的预加载器。

  • href:资源的路径。

  • as:资源的类型。

    示例代码中 preload 和 prefetch 了 CSS 和 JavaScript 文件,并且在页面中就立刻调用了 preload 加载的资源。而 prefetch 的资源,在页面中则没有立刻被调用,prefetch 的 news.js 文件只是提前加载下来,保存到了浏览器的缓存中,以便稍后在访问 news (相关)页面时可以立即解析它。

    除了 prefetch 和 preload CSS 和 JS 外,还可以加载很多其它类型的资源文件。

4.2、Preload 和 Prefetch 支持哪些类型的资源?

    prefetch 和 preload 可以支持许多不同的资源类型,在 <link> 元素中使用 as 属性设置资源类型,可选的值为:

  • audio音频文件,通常在中使用<audio>

  • document打算由<frame>或嵌入的HTML文档<iframe>浏览器未实现

  • embed要嵌入<embed>元素内的资源。

  • fetch通过提取或XHR请求访问的资源,例如ArrayBuffer或JSON文件。

  • font字体文件。

  • image 图像文件。

  • object要嵌入<object>元素内的资源。

  • scriptJavaScript文件。

  • styleCSS样式表。

  • trackWebVTT文件。

  • workerJavaScript网络工作者或共享工作者。

  • video视频文件,通常在中使用<video>。 浏览器未实现

    另外,<link> 元素可以接受 type 属性,该属性包含元素指向的资源的 MIME 类型。这在预加载资源时特别有用,浏览器将使用 type 属性值来计算是否支持该资源,并且仅在支持的情况下才下载该属性,否则将忽略该属性。

<head>    <meta charset="utf-8">    <title>Video preload example</title>    <link rel="preload" as="image" href="/favicon.ico" type="image/x-icon">    <link rel="preload" as="image" href="/static/images/app.svg" type="image/x-svg">    <link rel="preload" as="image" href="/static/images/logo.png" type="image/png">    <link rel="preload" href="sintel-short.mp4" as="video" type="video/mp4">    <link rel="preload" href="sintel-short.webm" as="video" type="video/webm"></head>
<body> <video controls> <source src="sintel-short.mp4" type="video/mp4"> <source src="sintel-short.webm" type="video/webm"> <p>Your browser doesn't support HTML5 video. Here is a <a href="sintel-short.mp4">link to the video</a> instead. </p> </video> </body>

    在上面 DEMO 页面的使用场景下,支持 MP4 的浏览器将预加载并使用 MP4 格式的文件,从而有望使视频播放器对用户更流畅/响应更快。而不支持 MP4 的浏览器则仍然可以加载 WebM 版本的资源,但是无法获得预加载的优势。这个示例列举了如何将预加载的内容与渐进增强的原理相结合。

4.3、启用 CORS 资源提取

    除了提取相同域名下的资源, preload 和 prefetch 还支持预加载其它域名的资源。启用 CORS 资源提取,例如:fetch()XMLHttpRequest 或字体。这个时候,就需要特别注意,需要设置 crossorigin 属性到 <link> 标签

<head>    <meta charset="utf-8">    <title>Web font example</title>    <link rel="preload" href="fonts/cicle_fina-webfont.woff2" as="font" type="font/woff2" crossorigin>    <link rel="preload" href="fonts/zantroke-webfont.woff2" as="font" type="font/woff2" crossorigin>    <link href="style.css" rel="stylesheet"></head>
<body> … </body>

    需要特别注意的是,在调用跨域的字体资源的时候,即使获取的字体资源不是跨源的,也需要将属性设置为匹配资源的 CORS 和凭据模式。

五、应用与实践

    在了解完 preload 和 prefetch 对提高前端性能的帮助和使用方法后,接下来就介绍一下在 DevOpen 平台使用 prefetch 和 preload 进行预加载资源的实践。

5.1、DevOps 平台应该 Preload 和 Prefetch 哪些资源文件?

    首先,我们需要确定 DevOps 平台中需要 preload (预加载)或者 prefetch(预读取)哪些资源文件?其实在 prefetch 和 preload 介绍说明中已经给出了两个重要的指导原则:

  • preload:应该是每个用户和每个页面都会使用的,优先级高的资源。

  • prefetch:应该是用户使用频率比较高的模块的资源,(尽量)确保用户将来(一定)会用到,会花费额外的带宽。

    根据我们的实际情况,可以很快分析出了需要 preload 加载的资源文件列表:

  • 天眼系统的分析JS文件:由于跨域,需要配置 crossorigin 属性。

  • 初始化时公共的 JS 和 CSS 资源:preload 加载的主要资源,DevOps 平台初始化运行起来需要的基本资源,每个模块,每个页面都会使用到。

  • 公共的图片资源:虽然优先级不高,但是每个人都会用到的图片。

    选择 prefetch 加载的资源就要麻烦些了,用户使用频率比较高的模块的资源,这个并没有标准的答案。目前我们是通过天眼系统统计的 API 请求次数分析出来的 RDC 使用最频繁的模块资源:

  • 控制台模块:我的事务、流水线、环境以及我的全部项目。

  • 分支模块:分支列表、分支申请和编辑页面。

  • 环境模块:环境列表、环境详情。

  • 流水线模块:流水线列表、流水线详情。

  • 版本模块:版本列表、(项目/产品)版本详情。

    一些初始化不是必须,但是常用模块的公共资源文件。在确定了要加载哪些资源文件后,就可以正式开始进行相关配置处理了。

5.2、preload  资源的 Webpack 配置

    要在 Vue 项目的工程中支持 preload 和 prefetch 需要安装 html-webpack-plugin 插件和 @vue/preload-webpack-plugin 插件:

npm install --save-dev @vue/preload-webpack-plugin html-webpack-plugin

    安装完依赖的第三方插件后,就可以在  vue.config.js 文件中配置了。

5.3、初始化时公共的 JS 和 CSS 资源的 Preload 配置

// it can improve the speed of the first screen, it is recommended to turn on preload config.plugin('preload').tap(() => [{    rel: 'preload', // to ignore runtime.js         // https://github.com/vuejs/vue-cli/blob/dev/packages/@vue/cli-service/lib/config/app.js#L171     fileBlacklist: [/\.map$/, /hot-update\.js$/, /runtime\..*\.js$/], // initial, asyncChunks, all         include: 'initial'}])

    具体的 preload-webpack-plugin 插件的配置我这里就不过多的介绍,这里只对 RDC 的 配置做一下简要说明:

  • rel: 'preload':表示将使用 <link rel="preload"> 预加载资源。

  • fileBlacklist: [/\.map$/, /hot-update\.js$/, /runtime\..*\.js$/]:表示预加载时过滤掉这些文件。

  • include:'initial':表示预加载初始化需要的资源。另外还有:asyncChunks - 所有动态生成的资源,all - 所有的资源。

    这里 initial 的 Chunk 资源是通过 optimization.splitChunks 配置出来的:

config.optimization.splitChunks({    chunks: 'all',    cacheGroups: {        libs: {            name: 'chunk-libs',            test: /[\\/]node_modules[\\/]/,            priority: 10,            chunks: 'initial' // only package third parties that are initially dependent             },        elementUI: {            name: 'chunk-elementUI', // split elementUI into a single package                  priority: 20, // the weight needs to be larger than libs and app or it will be packaged into libs or app                   test: /[\\/]node_modules[\\/]?element-ui(.*)/ // in order to adapt to cnpm             },        commons: {            name: 'chunk-commons',            test: resolve('src/components'), // can customize your rules                  minChunks: 3, //  minimum common number                  priority: 5,            reuseExistingChunk: true        }    }})

    preload 配置中的 initial 就是这里 cacheGroups 里面的 chunk-libs、elementUI 和 commons,以及最终公共的 app 相关的 JS 和 CSS 资源。打包后的结果:

<!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">    <meta name="keywords" content="RDC,DevOps,明源云,研发协同平台,端到端的研发协同平台,提质,提效,统一,标准">    <meta name="description"        content="RDC - 明源云,研发协同平台,端到端的研发协同平台。涵盖了明源ERP定制开发的从构想到交付的一切所需,使多方团队如:产品、项目、ODC、一线人员云端的高效协作,实践敏捷、精益开发与DevOps,提升软件质量的同时提高交付效率申请试用">    <link href="/static/css/app.de3b91d0.css" rel="preload" as="style">    <link href="/static/css/chunk-libs.de43804f.css" rel="preload" as="style">    <link href="/static/js/app.7a98b62f.js" rel="preload" as="script">    <link href="/static/js/chunk-elementUI.4f7e0b4e.js" rel="preload" as="script">    <link href="/static/js/chunk-libs.ff5a65f2.js" rel="preload" as="script">    <link href="/static/css/chunk-libs.de43804f.css" rel="stylesheet">    <link href="/static/css/app.de3b91d0.css" rel="stylesheet"></head><body class="rdc-body"><noscript><strong>We're sorry            but 天际·DevOps平台 doesn't work properly without            JavaScript enabled. Please enable it to            continue.</strong>    </noscript>    <div id="app">    </div>    <script src="/static/js/runtime.325b008f.js"></script>    <script src="/static / js / chunk - elementUI .4 f7e0b4e.js "></script>    <script src=" / static / js / chunk - libs.ff5a65f2.js "></script>    <script src=" / static / js / app .7 a98b62f.js "></script></body></html>

    可以看到,我们配置的 preload 的资源在页面中也立刻有被引用了。

5.4、公共的图片和天眼系统的统计脚本文件的 Preload 配置

    公共的图片和天眼系统的统计脚本文件的 preload  配置,就需要自己动手处理了。通过借助 html-webpack-plugin 插件,将 <link rel="preload"> 预加载资源的 HTML 字符串写入到 htmlWebpackPlugin.options.preloads 属性中, 然后到 /public/index.html 模板文件中输出:

<!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.0">    <meta name="keywords" content="<%= htmlWebpackPlugin.options.keywords %>">    <meta name="description" content="<%= htmlWebpackPlugin.options.description %>">    <!--预加载资源--> <%= htmlWebpackPlugin.options.preloads %>    <!--接入天眼-->    <script async src="https://mic-open.mypaas.com.cn/web-log-tracker/rdc-frontend/rdc-console/myWebLogTracker.min.js"></script>    <title><%= htmlWebpackPlugin.options.title %></title>    <link rel="icon" href="<%= BASE_URL %>favicon.ico"></head><body class="rdc-body"> </body></html>

    index.html 模板中只是输出:<%= htmlWebpackPlugin.options.preloads %>,而 vue.config.js 文件中的处理逻辑是这样的:

// 预加载的资源信息都保存在单独的位置文件中 const defaultSettings = require('./src/config/settings')module.exports = { // 其它逻辑     chainWebpack(config) {        config.plugin('html').tap(args => {            let preloads = [] // 收集预加载资源             defaultSettings.preloads.forEach((link) => {                let source = `<link rel="${link.rel}" as="${link.as}" href="${link.href}"`                if (link.type) {                    source += ` type="${link.type}"`                }                if (link.crossorigin) {                    source += ` crossorigin="${link.crossorigin}"`                }                source += '/>'                preloads.push(source)            }) args[0].keywords = defaultSettings.keywords args[0].description = defaultSettings.description args[0].title = defaultSettings.title args[0].version = guid // 输出资源加载的字符串             args[0].preloads = preloads.join('') return args        })    }    // 其它逻辑 }

    preload 需要加载的资源的配置变量都写入到了 defaultSettings.preloads 中了,这样可以方便以后需要预加载其它的资源,可以很灵活的在这个配置文件中添加编辑了。

const STATIC_PATH = '/static/img'const PRELOADS = [{    rel: 'preload',    href: 'https://mic-open.mypaas.com.cn/web-log-tracker/rdc-frontend/rdc-console/myWebLogTracker.min.js',    as: 'script',    crossorigin: 'anonymous'}, {    rel: 'preload',    href: `${STATIC_PATH}/rdc.png`,    as: 'image'}, {    rel: 'preload',    href: `${STATIC_PATH}/rdc-console.png`,    as: 'image'}, {    rel: 'preload',    href: `${STATIC_PATH}/loading.gif`,    as: 'image'}] module.exports = {    title: '天际·DevOps平台',    keywords: 'RDC,DevOps,明源云,研发协同平台,端到端的研发协同平台,提质,提效,统一,标准',    description: 'RDC - 明源云,研发协同平台,端到端的研发协同平台。涵盖了明源ERP定制开发的从构想到交付的一切所需,使多方团队如:产品、项目、ODC、一线人员云端的高效协作,实践敏捷、精益开发与DevOps,提升软件质量的同时提高交付效率申请试用',    preloads: PRELOADS}

    可以看到,preload 天眼系统的 js 脚本,因为启用 CORS 加载资源,所以设置了 crossorigin 属性。打包后最终生成的 index.html 文件是这样的:

<!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">    <meta name="keywords" content="RDC,DevOps,明源云,研发协同平台,端到端的研发协同平台,提质,提效,统一,标准">    <meta name="description"        content="RDC - 明源云,研发协同平台,端到端的研发协同平台。涵盖了明源ERP定制开发的从构想到交付的一切所需,使多方团队如:产品、项目、ODC、一线人员云端的高效协作,实践敏捷、精益开发与DevOps,提升软件质量的同时提高交付效率申请试用">    <link rel="preload" as="script"        href="https://mic-open.mypaas.com.cn/web-log-tracker/rdc-frontend/rdc-console/myWebLogTracker.min.js"        crossorigin="anonymous">    <link rel="preload" as="image" href="/static/img/rdc.png">    <link rel="preload" as="image" href="/static/img/rdc-console.png">    <link rel="preload" as="image" href="/static/img/loading.gif">    <script async src="https://mic-open.mypaas.com.cn/web-log-tracker/rdc-frontend/rdc-console/myWebLogTracker.min.js"></script>    <title>天际·DevOps平台</title>    <link rel="icon" href="/favicon.ico">    <!--其它 prefetch 的资源--></head><body>    <!--其它内容--></body></html>

    由于天眼系统要求它的分析脚本在页面最开始处解析,才能更准确的获得页面加载时间的数据,这里也只能做出点牺牲了。

5.5、prefetch  资源的 Webpack 配置

    使用 @vue/preload-webpack-plugin 插件生成预加载资源文件的时候(前面我们配置 preload 资源的时候),vue-cli 默认会将所有的 asyncChunks 资源都 prefetch 了。与此同时,而 DevOps 平台为了优化性能,使用了路由懒加载配置(这里以应用模块路由配置为例):

import Layout from '../path/to/the/layout'export default {    path: 'applications',    component: Layout,    children: [{        path: '',        component: () => import( /* webpackChunkName: "applications" */            '@/path/to/the/applications') // component: Applications        }, {        path: ':applicationId',        component: () => import( /* webpackChunkName: "applicationDetail" */            '@/path/to/the/dashboard') // component: Dashboard        }, {        path: ':applicationId/details',        component: () => import( /* webpackChunkName: "applicationTplDetail" */            '@/path/to/the/dashboard-template') // component: TemplateDashboard        }, {        path: ':applicationId/edit',        component: () => import( /* webpackChunkName: "applicationEdit" */            '@/path/to/the/edit') // component: Edit        }]}

    这样会生成110多个动态资源(asyncChunks ),所以使用默认的 prefetch 配置,会给用户带来很多额外的 HTTP 请求,造成不必要的带宽浪费,也增加了服务器的压力。所以这里要添加配置过滤掉不需要的资源:

// 因为默认 prefetch 了所有 asyncChunks,// 所以只能通过在 fileBlacklist 中排除不需要的资源的方式来配置 config.plugin('prefetch').tap(options => {    options[0].fileBlacklist = [ // 过滤用户不常用页面的资源            /[Mm]anagement.*\..*\.(js|css)$/, /team.*\..*\.(js|css)$/,        /profile.*\..*\.(js|css)$/, /defects.*\..*\.(js|css)$/,        /demands.*\..*\.(js|css)$/, /tasks.*\..*\.(js|css)$/,        /report.*\..*\.(js|css)$/, /quality.*\..*\.(js|css)$/,        /accountBook.*\..*\.(js|css)$/, /EnvironmentHistory.*\..*\.(js|css)$/,        /EnvironmentLogs.*\..*\.(js|css)$/, /EnvironmentDatabase.*\..*\.(js|css)$/,        /projectEnvironment.*\..*\.(js|css)$/, /projectMembers.*\..*\.(js|css)$/,        /projectSetting.*\..*\.(js|css)$/, /quickEntry.*\..*\.(js|css)$/,        /analysis.*\..*\.(js|css)$/, /application.*\..*\.(js|css)$/,        /resource.*\..*\.(js|css)$/    ]    return options})

    之所使用修改 fileBlocklist 的方式过滤掉不需要的资源,是因为 vue-cli 默认 prefetch 了所有 asyncChunks,目前发现只能通过在 fileBlacklist 中排除不需要的资源的方式才能成功(官方文档的示例也有点问题)。来看看最终效果:

<!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">    <meta name="keywords" content="RDC,DevOps,明源云,研发协同平台,端到端的研发协同平台,提质,提效,统一,标准">    <meta name="description"        content="RDC - 明源云,研发协同平台,端到端的研发协同平台。涵盖了明源ERP定制开发的从构想到交付的一切所需,使多方团队如:产品、项目、ODC、一线人员云端的高效协作,实践敏捷、精益开发与DevOps,提升软件质量的同时提高交付效率申请试用">    <link rel="preload" as="script"        href="https://mic-open.mypaas.com.cn/web-log-tracker/rdc-frontend/rdc-console/myWebLogTracker.min.js"        crossorigin="anonymous">    <link rel="preload" as="image" href="/static/img/rdc.png">    <link rel="preload" as="image" href="/static/img/rdc-console.png">    <link rel="preload" as="image" href="/static/img/loading.gif">    <script async src="https://mic-open.mypaas.com.cn/web-log-tracker/rdc-frontend/rdc-console/myWebLogTracker.min.js"        crossorigin="anonymous"></script>    <title>天际·DevOps平台</title>    <link rel="icon" href="/favicon.ico">    <!--常用模块 prefetch-->    <link href="/static/css/EnvironmentDetails.90395b51.css" rel="prefetch">    <link href="/static/css/EnvironmentDetails~pipelineDetail.3b940ed0.css" rel="prefetch"> // 省略了很多其它的常用模块资源    <link href="/static/js/versions.b5505c10.js" rel="prefetch">    <!–preload initial 资源-->        <link href="/static/css/app.4738dde5.css" rel="preload" as="style">        <link href="/static/css/chunk-libs.b7e15ced.css" rel="preload" as="style">        <link href="/static/js/app.914b34e8.js" rel="preload" as="script">        <link href="/static/js/chunk-elementUI.4f7e0b4e.js" rel="preload" as="script">        <link href="/static/js/chunk-libs.077e128e.js" rel="preload" as="script">        <!– 立刻调用 preload initial css 资源-->            <link href="/static/css/chunk-libs.b7e15ced.css" rel="stylesheet">            <link href="/static/css/app.4738dde5.css" rel="stylesheet"></head><body class="rdc-body">    <!--其它内容-->    <script src="/static/js/runtime.a6577107.js"></script>    <!– 立刻调用 preload initial js 资源-->        <script src="/static/js/chunk-elementUI.4f7e0b4e.js"></script>        <script src="/static/js/chunk-libs.077e128e.js"></script>        <script src="/static/js/app.914b34e8.js"></script></body></html>

5.6、验证效果

    可以清晰的看到,Network 面板的 Size 数据列显示为了 prefetch cache 就表明了我们的 prefetch 配置设置生效了。加上(测试环境)开启了 HTTP/2 的支持,prefetch 资源的时候,服务器的响应速度也是特别快的。与此同时,我们对静态资源(js / css)又启用了 GZip 压缩,并且开启了浏览器缓存,用户在第二次打开页面的时候,所有这些资源都是瞬间加载完毕的(下图的网络加载瀑布图几乎成了一条直线了。)。

    按照前面 preload 加载资源带来的性能提升方法计算,在开启了 HTTP/2 的情况下,资源加载优化的时间大概就是:86 + 221 + 251 (这仅仅是资源加载时间,没有计算 JS 执行消耗的时间),超过 550ml 了。来看看一组对比图:


上图:未启用 preload 预加载


上图:启用了 preload 预加载

    未启用 preload 是,JS 资源加载顺序会按照 html 代码调用 js 的顺序加载,前一个js加载并解析完,才加载第二个,启用 preload 后,浏览器加载前 4 个JS脚本几乎是同时请求的,效果还是很明显的。在所有文件大小都一样的情况下,启用了 preload 预加载(如图)DOMContentLoaded 加载时间大约提升了273ms,应当算很不错了。至于 load 时间的增加,是因为在“空闲时”,浏览器加载了 50 多个 prefetch 预取用的资源文件消耗的。而这些资源的加载,会在用户稍后访问相关页面提升性能。

    查看完性能的提升后,再来看看资源的加载顺序,如图所示,浏览器首先加载的是天眼系统的JS脚本和公共的图片,然后加载的是我们配置的 CSS 和 JS 资源,按照我们配置的顺序:

<link href="/static/css/app.4738dde5.css" rel="preload" as="style"><link href="/static/css/chunk-libs.b7e15ced.css" rel="preload" as="style"><link href="/static/js/app.914b34e8.js" rel="preload" as="script"><link href="/static/js/chunk-elementUI.4f7e0b4e.js" rel="preload" as="script"><link href="/static/js/chunk-libs.077e128e.js" rel="preload" as="script">

    这一点我们可以清晰的从脚本资源加载的顺序看到,在页面中的调用顺序是:

<script src="/static/js/runtime.a6577107.js"></script><!– 立刻调用 preload initial js 资源-->    <script src="/static/js/chunk-elementUI.4f7e0b4e.js"></script>    <script src="/static/js/chunk-libs.077e128e.js"></script>    <script src="/static/js/app.914b34e8.js"></script>

    而我们的网络请求的图片是这样的:

    可以看到,在加载完了所有 preload 的资源和页面中另外需要的 runtime.js 文件后,初始化界面所需要的资源就加载完成了。在浏览器开始“空闲”时,就开始加载 prefetch 配置的资源了。虽然 js 和 css 文件在前文我们介绍资源优先级别的时候应该是 Hightest 的,但在当前页面(工作台首页),预加载的环境详情相关的资源文件对于当前页面来说就是 Lowest 的级别了。因为这些资源并不是当前页面需要的资源,而是为用户访问环境详情页面,提前加载的资源。只有当我们访问环境详情页面时这些资源的优先级就又是 Highest 了。如下图:

    至于使用 pefetch 优化的效果,从上图也可以明确的看到,prefetch cache 缓存的资源的加载速度非常的快了,大约都是几毫秒就加载完成,比 disk cache 要快很多,比 memory cache 缓存稍微慢一点点。在页面第一加载的时候,浏览器缓存还没有应用的时候(没有 memory cache 和 disk cache),prefetch 加载的资源的效果也很明显了。

六、后续的思考

    细心的朋友会发现,我们的优化方案里没有预加载(preload)字体资源,这是因为字体资源并不是每个页面都需要的,虽然字体资源优先级级别较高,而且我们已经设置 font-display: swap,优化了字体加载的体验。

    目前 prefetch 的资源虽然是用户使用最频繁的模块的资源,但也并不能保证一定就是每个用户都会使用到,还是有可能会造成额外的带宽花费。预计的比较理想的处理方式是,用户登录后,在获取用户的类型后,根据不同用户类型的,在结合天眼系统的分析结果,统计出不同类型用户使用模块的频繁程度,只 prefetch 每类用户经常用的模块,尽量缩小需要 prefetch 资源的范围。

    另外,资源提示(Resource Hints)伪指令,除了预加载资源(preload/prefetch),还有 preconnect 和 dns-prefetch 可以用来优化跨域请求资源的 DNS 解析性能。在天际 DevOps 平台中,引用了天眼系统和所有用户头像图片都是调用的跨域资源,在后续也可以进行进一步的尝试和优化。 

    使用预加载资源技术只是优化了第一次加载页面的速度。如果需要优化再次访问的优化速度,就需要结合启用浏览器缓存。还需要启用 gzip 压缩,尽量减少静态资源的体积。还有我们 prefetch 了这么多的资源文件,提高服务器并发的响应能力就是需要考虑的了。而启用 HTTP/2,借助它的连接复用(多路复用),并行、Header 压缩 等特性,又可以进一步提高资源的加载响应速度。前端性能优化是一个长期且系统的工程,不是单独使用哪一个优化手段就可以一蹴而就的。

PS:以上提到的各种前端性能优化手段的技术细节,在接下来的前端性能优化系列技术文章里都会一一和大家分享,尽请关注!


----- END ------


作者简介

姚同学: 研发工程师,目前负责天际-DevOps平台的前端研发工作。


也许您还想看

技术分享 | 构建图表组件生态化的技术实战

【技术分享】前端大文件断点续传实践

浏览器缓存机制的研究分享

微前端架构在容器平台的应用

前端数据层落地实践


开发者头条

程序员分享平台