你不可能知道的骨架屏玩法🐶

33,016 阅读13分钟

〇 前言

这篇是作者在公司做了活动架构升级后,产出的主文的前导第二篇,考虑到本文相对独立,因此抽离出单独成文。姐妹兄弟篇,《你可能不知道的动态组件玩法🍉》。

可能文“对不起”题,知道的大佬别进来了😂,不、不,求吐槽、拍砖,瑞思拜。

感谢昊神赐题😂,不要怪ssh,怪我。

image.png

本文可能存在有些纰漏,希望大家多拍砖、建议,谢谢😘。

〇 背景

作者曾所在我司广告事业部,广告承载方式是以刮刮卡、大转盘等活动页进行展示,然后用户参与出广告券弹层。

image.png

这篇文章主要背景是这样的,有业务方反馈,提议我们能不能做一些对页面流失率有提升的优化。

因此针对活动页面的数据情况,我们去做了测试。从测试数据反映,有些页面加载完成率(专业的可以理解为首屏加载率)偏低,但活动页面上一级入口点击率正常。

这种情况有点奇怪啊,但经验告诉我们,一般就是用户在点击上一级入口进来后,由于等待白屏时间过长,用户可能以为白屏是挂了或者忍受不了等待白屏的焦虑,没有耐心就流失了。

image.png

怎么去缩短白屏时间呢?

那就是让用户可以更快看到非“白色”,可以能联想到的,背景底色、背景图、关键部位图等。

不知道大家有没有使用过骨架屏,下面我们就是用类似骨架屏的能力去解决这个问题。

〇 “骨架图”实现

骨架屏基本就是详细页面元素未展现时,把DOM结构通过线条勾勒出来。而对于C端的营销类活动页面来说,并没有比较标准的骨架,每个活动有自己的轮廓,那怎么办呢?

我们可以通过背景色和图片来达到类似的功效,因此我们衍生出“骨架图”的概念,其实也是一种骨架屏。

image.png

实现思路

以一个拆红包的活动去看,我们会发现用户关注的内容,是图中的“拆字红包”和背景色。

image.png

我们应该尽量让“拆”字红包图更快的展示。

下面是这个活动的渲染截图,通过Chrome Dev Tools -> Network -> Disable Cache -> Fast 3G(4G、WIFI过快不易观察) -> 右侧 ⚙️ -> Capture ScreenShots。就可以打开了。可以看到一帧一帧的图片。

我们目的是想让关键帧,下图中的绿色框中的1.44s那帧可以更早展现。

image.png

怎么形成这么一帧关键图片呢🤔 ?可以很自然的想到,一张静态页面。无非通过HTML、CSS、图片渲染而成。

image.png

所以需要提供DOM结构、提供CSS、提供图片,生成“静态骨架图”。

image.png

上面是一个普通的HTML开发。

🤔️ 但我们不是在通过纯HTML开发,怎么才能拿到Vue页面的DOM结构呢?

这里可能同学们有疑问,为什么需要单独拿Vue的DOM结构。

我们一般一个Vue项目一般都是挂载在某个根节点下,比如#app下。

<html>
  <head></head>
  <body>
    <div id="app"></div>
    <script src="/cdn/xxx/vue.js"></script>
  </body>
</html>

通过把Vue实例挂载在#app上。

// index.js
import Entry from './Entry.vue'

new Vue({
  render: h => h(Entry)
}).$mount('#app')

然后才是真正这个组件对应的DOM结构(template)。

// Entry.vue
<template>
  <div class="entry">
    <img src="/cdn/xx/image.png"/>
    <button class="btn" type="button">请点击</button>
  </div>
</template>

<script>
export default {
  data() {
    return {
    }
  }
}
</script>

从上面可以看出Vue的DOM结构实际是隐藏在.vue文件里的,而我们初始化渲染Vue前,实际只能拿到#app这个div。

因此我们需要需要想办法拿到Vue组件里面的DOM结构。怎么拿呢?

image.png

预渲染DOM

在开始前,先看看这个图,表示了我们大致的流程,有图不迷路😂。

image.png

不知道大家有没有听说过puppeteer,一个无头浏览器,它能做什么呢?一般我们会使用它去运行线上目标页面,去抓取一些数据。

这里我们利用它,去帮我们截取Vue的DOM结构。自己我们去使用puppetter去截取DOM会需要做几个步骤,用无头浏览器跑对应的页面,然后等页面把Vue组件渲染出,渲染完成把对应的#app下的DOM结构截取出来,然后保存下来。

由于步骤并不难,但会涉及到挺多代码,其实社区里已经有大佬帮我们把这些能力集成了,prerender-spa-plugin,就是它了。

image.png

使用prerender-spa-plugin可以很容易的拿到DOM结构,它的原理就是先运行无头浏览器,然后执行对应的App路由,截取展示出的页面中的DOM结构。

prerender-spa-plugin的具体用法在这里就不细讲了,可以参考官方文档。

cheerio是一个方便我们获取内容的工具,看看官方解释。

为服务器特别定制的,快速、灵活、实施的jQuery核心实现。

要获取Vue页面的DOM结构,需要分两步。

  1. 先预渲染构建,输出预渲染获取到的Vue页面的关键DOM结构。
  2. 再正常构建把获取到的DOM结构插入到页面初始DOM上。

预渲染构建

image.png

下面是在预渲染构建里,webpack的预渲染配置。

// 预渲染构建的配置

{
  plugins: [
    // 生成vue dom骨架
    new PrerenderSPAPlugin({
      // 原文件地址
      staticDir: path.join(__dirname, config.localDir),
      // 构建生成地址
      outputDir: path.join(__dirname, config.prerenderPath),
      routes: [ '/' ],
      // 获取上下文
      postProcess (context) {
        // 获取编译后的html内容
        const html = context.html
				// 使用cheerio选择器
        const $ = cheerio.load(html)
        // 预渲染删除apple(一般C端页面都有个苹果免责协议,在预渲染的页面是多余的)
        $('#apple').remove()
        // 截取需要的html片段
        const contentHtml = '<div class="app">' + $('.app').html() + '</div>'
        // 删除掉一些首屏无关的image图片
        context.html = contentHtml.replace(/<img[^>]*>/gi, '')
				// 匹配每个标签里的style里的内容
        context.html = context.html.replace(/style="[^"]*"/g, function (matched) {
          // 如果符合以下布局属性,则保留,这里没考虑margin、padding等
          const reg = /(width|height|left|top|bottom|right):[^rem]*rem;/g
          const result = matched.match(reg)
          if (result) return 'style="' + result.join('') + '"'
          return ''
        })
        
        // 输出处理好的预渲染内容
        return context
      }
    })]
}

预渲染构建好了,拿到了Vue页面的DOM结构,我们开始正式构建。

正式构建

image.png

这里我们自己写个Webpack插件,关于怎么写Webpack插件这里不赘述了。功能主要是把预渲染生成的DOM,插入到正式DOM中。

// 正式构建,利用插件的能力

const path = require('path')
const fs = require('fs')

// 命名插件,也可以直接使用class定义,不是重点
function VueDomPrerenderPlugin (options) {
  this._options = options
}

VueDomPrerenderPlugin.prototype.apply = function (compiler) {
  const self = this
  compiler.hooks.compilation.tap('VueDomPrerenderPlugin', (compilation) => {
    // 通过html-webpack-plugin的hook
    compilation.plugin(
      'html-webpack-plugin-after-html-processing',
      (data, cb) => {
        // 找到预渲染输出的文件
        const prerenderFile = path.join(self._options.preoutDir, 'index.html')

        // 把预渲染生成的DOM,插入到正式DOM中。
        let htmlContent = data.html.replace('<div id="app"></div>', (matched) => {
          const prerenderHtml = fs.readFileSync(prerenderFile, 'utf8')
          return '<div id="app" data-server-render="true">' + prerenderHtml + '</div>'
        })
      }
    )
  })
}

module.exports = VueDomPrerenderPlugin

但现在的是静态的,页面上的图片、背景色都是定死的,怎么才能动态设置图片、背景色等呢?

image.png

动态设置数据

image.png

HTML、CSS只能做静态页面,但JavaScript可以啊,JS可以拿到数据,根据数据进行设置,那我们的页面就不是硬编码的图片和颜色了。

首先通过JS拿到对应的图片、颜色数据,再找到DOM结构上对应的图片占位等,通过JS进行图片设置,这里就有三步。

// H5页面中的代码

let color = window.CFG.color
let bgImage = window.CFG.bgImage
let bgColor = window.CFG.bgColor
// 类似的还有其他数据...

let $text = document.querySelector('.text')
let $bg = document.querySelector('.bg')
// 类似的还有很多...

$text.style.color = color
$bg.style.backgroundImage = 'url(' + bgImage + ')'
$bg.style.backgroundColor = bgColor
// 类似的还有很多...

🤔️ 通过上述JS这种命令式的方式,在样式设置上的可读性差;而且我们模版代码太多了,对业务侵入性比较强,对开发很不友好。怎么办呢?

image.png

减少重复代码

我们在思考,能不能就让开发同学,书写代码像写css一样,编码去预设一些预加载的图片和背景色等等呢?

可以理解把上面的抽象点,可以让命令式的代码变成声明式的代码,比较利于理解。

大致的流程。

image.png

定义模版

image.png

我们想到了,能不能利用模版的能力,提供一个.tcss文件类型,这是一个类CSS的文件。可以看到我们通过**{{ }}**来提供变量设置能力。

image.png

image.png

解析模版

image.png

那怎么解析这样一个模版呢,我们通过node以及正则表达式的能力。需要提供一段代码逻辑,先读取.tcss文件,然后通过替换模版为真实可运行代码。

// node脚本中的代码
// 解析preload.tcss,输出preloadCss、preloadImages

// 某个活动下
const BASE_FOLDER = `./src/pages/activity`
// 上述文件.tcss所在地址
const cssPath = path.join(BASE_FOLDER, 'preload.tcss')
// 获取文件的字符串
const data = fs.readFileSync(cssPath).toString()

// 匹配字符串的开始
let start = 0
// 记录预加载的图片
const preloadImages = []

let preloadCss = data.replace(/{{(\S+)}}/g, function (matched, pattern, offset, string) {
  // 当前匹配到的字段的偏移量
  let end = offset

  // 截取非空字符串
  const substring = string.substring(start, end)

  // 只有image类型的才会存到预加载数组里
  if (substring.indexOf('url') !== -1) {
    preloadImages.push(`get('${pattern}')`)
  }

  // 上一次的终点为这次的起点
  start = end

  // 把匹配到的例如image、color进行包装
  return `' + get('${pattern}') + '`
})

// 统一成\n
preloadCss = preloadCss.replace(/(\r|\n|\r\n)/g, function (matched) {
  return `'\n+'`
})

大致代码如上,会输出生成预加载的图片列表 - preloadImages、预加载样式Style的JS片段 - preloadCss。

生成style的JS片段,这里大家可能会奇怪怎么是生成这样的一段JS代码,是因为我们通过node脚本,先在本地预先构建了可以“生成CSS的JS代码”,最终这段代码是页面渲染的时候运行。为什么不是纯CSS,因为我们需要动态拿属性值(image、color等)。

preloadCss大致如下图所示。

image.png

preloadImages大致如下图所示。

image.png

image.png

生产物料的代码(图片、CSS)

这里图片,我们选择了用最简便的new Image去实现。

image.png

拿到了preloadImages、preloadCss后,我们再调用公共方法去加载图片、生成style片段。

// node脚本中的代码

const TARGET_PATH = './node_modules/.cache/preload-image/'

const outputFilePath = path.join(TARGET_PATH, 'index.js')

if (!fs.existsSync(TARGET_PATH)) {
  fs.mkdirSync(TARGET_PATH, { recursive: true })
}

const preloadCode = `
;(function(win) {
  var preloadImages = [${preloadImages.join(',')}];

  preloadImage(preloadImages);

  var styles = '${preloadCss}';

  addPreloadStyle(styles);
})(window);
`

fs.writeFileSync(outputFilePath, preloadCode)

上面的代码拼接成一串字符串,最终会通过内嵌到HTML页面中,在页面渲染时运行。

这样我们就完成了预加载的物料:图片、样式的准备了。下面需要把准备应用上到页面上。

image.png

内联代码到页面

image.png

把设置预加载图片、样式的JS以内联的方式潜入到HTML。

<html>
  <head>
	<script src="./node_modules/.cache/preload-image/index.js?__inline"></script>
  </head>
</html>

大家可以看到?__inline是做什么的呢,它的效果就是把外联JS内联到HTML中。

<html>
  <head>
    <script>
      function preloadImage (arr) {
        for (let i = 0; i < arr.length; i++) {
          const images = []
          images[i] = new Image()
          images[i].src = arr[i]
        }
      }

      function addPreloadStyle (styles) {
        const css = document.createElement('style')
        css.type = 'text/css'

        if (css.styleSheet) {
          css.styleSheet.cssText = styles
        } else {
          css.appendChild(document.createTextNode(styles))
        }

        document.getElementsByTagName('head')[0].appendChild(css)
      }
    </script>
		<script>
      !function(win){
        var preloadImages = [
          get("bgImage"),
          // 省略了...
        ];
        preloadImage(preloadImages);

        var preloadCss = '.tac-app {' +  
            'background-image: url("'+ get("bgImage")+'");' +  
            'background-color: '+ get("bgColor")+';' +
            '}'
        addPreloadStyle(preloadCss)
      (window);
    </script>
  </head>
</html>

image.png

「福利:__inline功能的webpack插件」

关于上面?__inline功能是怎么实现的,可以看看这个webpack插件的写法。

/*
 * script标签中含有?__inline标识的Js会被内联到HTML。
 */
const fs = require('fs')

const getScriptAbsolutePath = (matched, reg) => {
  const result = matched.match(reg)
  const relativePath = result && result[1]

  return relativePath
}

class ScriptInlinePlugin {
  apply (compiler) {
    compiler.hooks.compilation.tap('ScriptInlinePlugin', (compilation) => {
      // 由于vuecli3使用的webpack-html-plugin是3.2版本,所以暂时不能使用tap形式。
      compilation.plugin(
        'html-webpack-plugin-after-html-processing',
        (data) => {
          // 读取目标模版
          let htmlContent = data.html

          // 匹配script的reg
          const jsReg = /<script[^<]*?src="?[^<]*?\?__inline"?.*?>.*?<\/script>/gmi

          // 匹配script片段
          htmlContent = htmlContent.replace(jsReg, (matched) => {
            // 获取script绝对地址
            const absolutePath = getScriptAbsolutePath(matched, /src="?(.*)\?__inline/)

            // 获取script对应地址的内容
            const jsContent = fs.readFileSync(absolutePath, 'utf-8')

            return `<script type="text/javascript">${jsContent}</script>`
          })

          data.html = htmlContent
        }
      )
    })
  }
}

module.exports = ScriptInlinePlugin

基本的功能已经具备了,但使用了一段时间后,发现有些问题。

体验上的考虑

我们发现功能是实现了,但对于开发体验不太友好。

分析步骤

现在开发同学需要开发一个骨架屏,需要几个步骤。

  1. vue中设定预加载图片的placeholder,这样才能关联上预加载好的图片。
<template>
  <div :class="$style.app">
    <div :class="$style.button">
    </div>
    <div :class="$style.cat">
    </div>
  </div>
</template>

<style lang="less" module>
// 预渲染使用的全局class占位符
:global {
  .app {}
  .button {}
  .cat {}
}
</style>
  1. 需知道获取后端接口返回的哪些变量,然后设置到对应的.tcss文件里
// backgroundColor
// backgroundImage

// buttonImage

// catImage
  1. tcss设定书写对应的预加载样式
.app {
  background-image: url("{{backgroundImage}}");
}

.button {
  background-image: url("{{buttonImage}}");
}

.cat {
  background-image: url("{{catImage}}");
}

image.png

这对开发同学的心智成本还是比较大的,他需要关心的太多了。

前置信息

下图是一个活动的主要流程。

image.png

抛开细节,关注关键活动流程。

  1. 用户进入活动页面,服务器开始渲染;
  2. 活动开发提供活动代码,基建开发提供公共代码;
  3. 活动运营通过运营管理平台配置活动;
  4. 服务器开始模版拼接,从数据库获取代码及配置进行组合;
  5. 浏览器展现活动页面。

因为我们一个活动是固定主图和样式的,并没有千人千面。

所以我们可以在运营管理平台配置的时候就确认哪些图片可以被预先加载。

配置代替编码

可以在运营管理平台配置,选择需要预加载的图片,比如下图的背景图。

image.png

把所有选择的Image作为一个列表,传入后端SSR,由于我们Java后端作为模版渲染,使用的Velocity模版。

// 通过Velocity循环渲染。
#foreach( $key in $preloadImageList )
    <link rel="preload" as="image" href="$preloadImageList.get($key)">
#end

这样我们就让一线同学从编码预加载的负担中,解放了出来。

工程化的事情完工了,下面我们需要看看怎么优化我们的性能了,更主要的是在于图片。

〇 更快的图片加载

注意⚠️,下文中关于network的查看,先进行如下操作,Chrome Dev Tools -> Network -> Disable Cache -> Fast 3G。

资源加载顺序

上文我们通过new Image的方式来进行图片的加载,但会遇到问题。

new Image加载

我们通过个例子来了解下这个问题。这个页面需要加载3张图片、2个CSS文件、6个JS文件。

<!DOCTYPE html>
<html>
<head>
    <script>
        function preloadImage(arr) {
            for (let i = 0; i < arr.length; i++) {
                const images = []
                images[i] = new Image()
                images[i].src = arr[i]
            }
        }
        preloadImage(
            [
                'http://yun.tuisnake.com/tuia/dist/image/1.jpg',
                'http://yun.tuisnake.com/tuia/dist/image/2.png',
                'http://yun.tuisnake.com/tuia/dist/image/3.png'
            ])
    </script>
    <link rel="stylesheet"
			href="http://yun.tuisnake.com/tuia/dist/css/bootstrap1.css">
    <link rel="stylesheet"
			href="http://yun.tuisnake.com/tuia/dist/css/bootstrap2.css">
</head>

<body>
    <script src="http://yun.tuisnake.com/tuia/dist/js/vue1.js"></script>
    <script src="http://yun.tuisnake.com/tuia/dist/js/vue2.js"></script>
    <script src="http://yun.tuisnake.com/tuia/dist/js/vue3.js"></script>
    <script src="http://yun.tuisnake.com/tuia/dist/js/vue4.js"></script>
    <script src="http://yun.tuisnake.com/tuia/dist/js/vue5.js"></script>
    <script src="http://yun.tuisnake.com/tuia/dist/js/vue6.js"></script>
</body>
</html>

我们发现图片是在前四个JS文件下载完,才开始下载。但我们希望图片可以更早的下载。

image.png

对上图不了解的同学,可以阅读下,Timing breakdown phases explained

这是为什么呢?首先,在Http1.1协议中,同域名下同时只能打开6个TCP链接,因此需要进行资源排队,可以看到上面的红色框是在排队的。

资源是有优先级的

但这好像也不能足够说明吧🧐,我们继续寻找原因。

image.png

我们发现有个字段叫Priority,图片资源是Low,看样子图片好像是排队里优先级最低的。

image.png

但这个优先级是干啥用的啊?

想一想🤔️我们的浏览器是怎么知道哪些资源先下载的呢?这里不卖关子了。

  1. 首先要清楚对浏览器资源的进行分门别类
  2. 然后对资源的优先级进行计算
  3. 最后根据资源的优先级进行顺序地下载

这里扩展阅读可以看看:浏览器页面资源加载过程与优化

从而我们发现浏览器是有资源加载优先级的,比如 CSS、HTML、JS等都是核心资源,所以优先级最高;而图片、视音频就不是核心资源,优先级就比较低。通常当后者遇到前者时,就需要“让路”,进入待排队状态。

图片的优先级

image.png

再回到本文,为啥我们的图片优先级是Low呢,能不能提升呢?那我们先来了解个知识点。

Avoid chaining critical requests里提到一份浏览器优先级细分报告(由Pat Meenan提供),显示了从Chrome 46及更高版本开始,Blink 内核的 Chrome 如何优先处理不同的资源。

下图就是上述文章里,有关Chrome的加载优先级,可以观摩一下。

PS:这张图是2015年的,可能现在浏览器的行为会有出入。希望有知道更新一版的同学请留言呀,谢谢。

image.png

我们可以看到Image有两种类型的优先级,一种是在视图内的 - Image(in viewport),另一种是视图外的 - Image。分别对应了High、Low优先级。

我们也在另外篇文章里发现了论证此点的线索,在web.dev的Fast load times模块中的Prioritize resources文章介绍。

for example, an image that is part of the initial render is prioritized higher than an image that starts offscreen.

背景图形式加载

理论都这么说,我们验证下。我们给3个图片都增加了样式、DOM、背景图设置,使得它们存在在视图内。

<!DOCTYPE html>
<html>
<head>
    <style>
        .image1, .image2, .image3 {
          height: 300px;
          width: 400px;
        }
        .image1 {
          background-image: url('http://yun.tuisnake.com/tuia/dist/image/1.jpg');
        }
        .image2 {
          background-image: url('http://yun.tuisnake.com/tuia/dist/image/2.png');
        }
        .image3 {
          background-image: url('http://yun.tuisnake.com/tuia/dist/image/3.png');
        }
    </style>
    <!-- 利用bootstrap作为测试css,有2个 -->
    <link rel="stylesheet"
			href="http://yun.tuisnake.com/tuia/dist/css/bootstrap1.css">
    <link rel="stylesheet"
			href="http://yun.tuisnake.com/tuia/dist/css/bootstrap2.css">
</head>

<body>
    <div class="image1"></div>
    <div class="image2"></div>
    <div class="image3"></div>
    <!-- 利用vue作为测试js,有6个 -->
    <script src="http://yun.tuisnake.com/tuia/dist/js/vue1.js"></script>
    <script src="http://yun.tuisnake.com/tuia/dist/js/vue2.js"></script>
    <script src="http://yun.tuisnake.com/tuia/dist/js/vue3.js"></script>
    <script src="http://yun.tuisnake.com/tuia/dist/js/vue4.js"></script>
    <script src="http://yun.tuisnake.com/tuia/dist/js/vue5.js"></script>
    <script src="http://yun.tuisnake.com/tuia/dist/js/vue6.js"></script>
</body>
</html>

结果如下图。

image.png

从上面图片看,确实是图片的优先级被提升到了High,但还是在js文件后面加载了,这是为什么?

以css背景图存在的图片background-image,会等到结构加载完成(网页的内容全部显示以后)才开始加载;而html中的标签img是网页结构(内容)的一部分,会在加载结构的过程中加载。

那我们试试直接使用img标签呢?

image.png

标签形式加载

<!DOCTYPE html>
<html>
<head>
    <!-- 利用bootstrap作为测试css,有2个 -->
    <link rel="stylesheet"
			href="http://yun.tuisnake.com/tuia/dist/css/bootstrap1.css">
    <link rel="stylesheet"
			href="http://yun.tuisnake.com/tuia/dist/css/bootstrap2.css">
</head>

<body>
    <!-- 修改点 -->
 	  <img src="http://yun.tuisnake.com/tuia/dist/image/1.jpg"/>
    <img src="http://yun.tuisnake.com/tuia/dist/image/2.png"/>
    <!-- 利用vue作为测试js,有6个 -->
    <script src="http://yun.tuisnake.com/tuia/dist/js/vue1.js"></script>
    <script src="http://yun.tuisnake.com/tuia/dist/js/vue2.js"></script>
</body>
</html>

我们看到图片确实是第一时间加载了。

image.png

真的是这样吗,图片如果是img标签形式就提前加载吗?

我们再换一个DEMO试试,2个CSS文件、4个JS文件。

<!DOCTYPE html>
<html>
<head>
    <!-- 利用bootstrap作为测试css,有2个 -->
    <link rel="stylesheet"
			href="http://yun.tuisnake.com/tuia/dist/css/bootstrap1.css">
    <link rel="stylesheet"
			href="http://yun.tuisnake.com/tuia/dist/css/bootstrap2.css">
</head>

<body>
    <!-- 修改点 -->
 	  <img src="http://yun.tuisnake.com/tuia/dist/image/1.jpg"/>
    <img src="http://yun.tuisnake.com/tuia/dist/image/2.png"/>
    <!-- 利用vue作为测试js,有6个 -->
    <script src="http://yun.tuisnake.com/tuia/dist/js/vue1.js"></script>
    <script src="http://yun.tuisnake.com/tuia/dist/js/vue2.js"></script>
    <script src="http://yun.tuisnake.com/tuia/dist/js/vue3.js"></script>
    <script src="http://yun.tuisnake.com/tuia/dist/js/vue4.js"></script>
</body>
</html>

我们看看结果。

image.png

我们发现,两个图片资源滞后了。这是为什么啊???

image.png

再回忆我之前这节前埋的伏笔,2个CSS+4个JS,已经占满了一个域名6个请求的限制,图片就滞后了。

那为什么图片抢不过CSS、JS呢?再回头看看这张图。

image.png

Image在初始化的时候默认优先级都为Low,只有等浏览器渲染到图片的时候,计算是否在视图内把图片提到优先级为High。有看出什么眉目吗,因为CSS、Script的优先级都会比Image来的高。

通过image加载图片也不是很可靠,还有其他办法吗?

image.png

优先图片加载

我们来看看一个API,preload。

方法一:preload加载图片

还可以阅读下这篇文章Preload, Prefetch And Priorities in Chrome了解,这有翻译版,Preload,Prefetch 和它们在 Chrome 之中的优先级性能优化

图片优先级提升了,但并不一定第一时间加载。怎么才能强制提升图片的加载顺序呢?

  1. html、css、font这三种类型的资源优先级最高;
  2. 然后是preload资源(通过<link rel=“preload">标签预加载)、script、xhr请求。
<!DOCTYPE html>
<html>
<head>
    <!-- 修改点 -->
    <link rel="preload" as="image" href="http://yun.tuisnake.com/tuia/dist/image/1.jpg">
    <link rel="preload" as="image" href="http://yun.tuisnake.com/tuia/dist/image/2.png">

    <!-- 利用bootstrap作为测试css,有2个 -->
    <link rel="stylesheet"
			href="http://yun.tuisnake.com/tuia/dist/css/bootstrap1.css">
    <link rel="stylesheet"
			href="http://yun.tuisnake.com/tuia/dist/css/bootstrap2.css">
</head>

<body>
    <!-- 利用vue作为测试js,有6个 -->
    <script src="http://yun.tuisnake.com/tuia/dist/js/vue1.js"></script>
    <script src="http://yun.tuisnake.com/tuia/dist/js/vue2.js"></script>
    <script src="http://yun.tuisnake.com/tuia/dist/js/vue3.js"></script>
    <script src="http://yun.tuisnake.com/tuia/dist/js/vue4.js"></script>
    <script src="http://yun.tuisnake.com/tuia/dist/js/vue5.js"></script>
    <script src="http://yun.tuisnake.com/tuia/dist/js/vue6.js"></script>
</body>
</html>

通过preload我们可以改变浏览器资源加载顺序。

image.png

可以从上图看到,我们的图片确实第一时间加载了。

image.png

还有其他办法吗?

方法二:单独图片域名

我们知道http1.1同域名下,限制6个链接,那我们可以试试多个域名?给图片另一个独特域名。

<!DOCTYPE html>
<html>
<head>
    <script>
        function preloadImage(arr) {
            for (let i = 0; i < arr.length; i++) {
                const images = []
                images[i] = new Image()
                images[i].src = arr[i]
            }
        }
    </script>
    <!-- 测试图片3张 -->
    <script>
        preloadImage(
            [
                'https://yun.tuipink.com/tuia/dist/image/1.jpg',
                'https://yun.tuipink.com/tuia/dist/image/2.png',
                'https://yun.tuipink.com/tuia/dist/image/3.png'
            ])
    </script>
    <!-- 利用bootstrap作为测试css,有2个 -->
    <link rel="stylesheet"
			href="https://yun.tuisnake.com/tuia/dist/css/bootstrap1.css">
    <link rel="stylesheet"
			href="https://yun.tuisnake.com/tuia/dist/css/bootstrap2.css">
</head>

<body>
    <!-- 利用vue作为测试js,有6个 -->
    <script src="https://yun.tuisnake.com/tuia/dist/js/vue1.js"></script>
    <script src="https://yun.tuisnake.com/tuia/dist/js/vue2.js"></script>
    <script src="https://yun.tuisnake.com/tuia/dist/js/vue3.js"></script>
    <script src="https://yun.tuisnake.com/tuia/dist/js/vue4.js"></script>
    <script src="https://yun.tuisnake.com/tuia/dist/js/vue5.js"></script>
    <script src="https://yun.tuisnake.com/tuia/dist/js/vue6.js"></script>
</body>
</html>

可以很明显看到图片也在第一时间进行了加载。

image.png

看起来也挺好的,是不是,还有没有其他方式呢?

方法三:降低其他资源优先级

通过优先级表,我们知道如果把JS延后加载,相对于就是提前了图片加载。

我们先可以考虑下async、defer标记。

image.png

async vs defer attributes - Growing with the Web

但发现async、defer并不会改变js文件请求的顺序,依旧是排在image前面。

「async」

<!DOCTYPE html>
<html>
<head>
    <!-- 利用bootstrap作为测试css,有2个 -->
    <link rel="stylesheet"
			href="http://yun.tuisnake.com/tuia/dist/css/bootstrap1.css">
    <link rel="stylesheet"
			href="http://yun.tuisnake.com/tuia/dist/css/bootstrap2.css">
</head>

<body>
    <image src="http://yun.tuisnake.com/tuia/dist/image/1.jpg"></image>
    <image src="http://yun.tuisnake.com/tuia/dist/image/2.png"></image>
    <image src="http://yun.tuisnake.com/tuia/dist/image/3.png"></image>
    <!-- 利用vue作为测试js,有6个 -->
    <script async src="http://yun.tuisnake.com/tuia/dist/js/vue1.js"></script>
    <script async src="http://yun.tuisnake.com/tuia/dist/js/vue2.js"></script>
    <script async src="http://yun.tuisnake.com/tuia/dist/js/vue3.js"></script>
    <script async src="http://yun.tuisnake.com/tuia/dist/js/vue4.js"></script>
    <script async src="http://yun.tuisnake.com/tuia/dist/js/vue5.js"></script>
    <script async src="http://yun.tuisnake.com/tuia/dist/js/vue6.js"></script>
</body>
</html>

图片依旧是最后加载。

image.png

「defer」

<!DOCTYPE html>
<html>
<head>
    <!-- 利用bootstrap作为测试css,有2个 -->
    <link rel="stylesheet"
			href="http://yun.tuisnake.com/tuia/dist/css/bootstrap1.css">
    <link rel="stylesheet"
			href="http://yun.tuisnake.com/tuia/dist/css/bootstrap2.css">
</head>

<body>
    <image src="http://yun.tuisnake.com/tuia/dist/image/1.jpg"></image>
    <image src="http://yun.tuisnake.com/tuia/dist/image/2.png"></image>
    <image src="http://yun.tuisnake.com/tuia/dist/image/3.png"></image>
    <!-- 利用vue作为测试js,有6个 -->
    <script defer src="http://yun.tuisnake.com/tuia/dist/js/vue1.js"></script>
    <script defer src="http://yun.tuisnake.com/tuia/dist/js/vue2.js"></script>
    <script defer src="http://yun.tuisnake.com/tuia/dist/js/vue3.js"></script>
    <script defer src="http://yun.tuisnake.com/tuia/dist/js/vue4.js"></script>
    <script defer src="http://yun.tuisnake.com/tuia/dist/js/vue5.js"></script>
    <script defer src="http://yun.tuisnake.com/tuia/dist/js/vue6.js"></script>
</body>
</html>

也是不行的,图片依旧最后加载。

image.png

上面两种方式不行,还有其他方式吗?

「JS Loader」

既然原生的不行,我们来JS代码控制Script、CSS插入的时机。

<!DOCTYPE html>
<html>
<head>
    <!-- 利用bootstrap作为测试css,有2个 -->
    <link rel="stylesheet"
			href="https://yun.tuisnake.com/tuia/dist/css/bootstrap1.css">
    <link rel="stylesheet"
			href="https://yun.tuisnake.com/tuia/dist/css/bootstrap2.css">
</head>

<body>
    <!-- 修改点 -->
    <image src="https://yun.tuisnake.com/tuia/dist/image/1.jpg"></image>
    <image src="https://yun.tuisnake.com/tuia/dist/image/2.png"></image>
    <image src="https://yun.tuisnake.com/tuia/dist/image/3.png"></image>

    <script src="https://yun.tuisnake.com/tuia/dist/js/loader.js"></script>
    <script>
        Loader.async([
            'https://yun.tuisnake.com/tuia/dist/js/vue1.js', 
            'https://yun.tuisnake.com/tuia/dist/js/vue2.js',
            'https://yun.tuisnake.com/tuia/dist/js/vue3.js', 
            'https://yun.tuisnake.com/tuia/dist/js/vue4.js',
            'https://yun.tuisnake.com/tuia/dist/js/vue5.js', 
            'https://yun.tuisnake.com/tuia/dist/js/vue6.js'
        ])  
    </script>
</body>
</html>

可以看到明显的图片优先加载了。

image.png

之前说了那么多都是针对Http1.1的,我们现在看看Http2.0情况下。

方法四:Http2.0

我们需要开启Http2.0,yun.tuiapple.com这个域名是开启了Http2.0的。

<!DOCTYPE html>
<html>
<head>
    <script>
        function preloadImage(arr) {
            for (let i = 0; i < arr.length; i++) {
                const images = []
                images[i] = new Image()
                images[i].src = arr[i]
            }
        }
    </script>
    <!-- 测试图片3张 -->
    <script>
        preloadImage(
            [
                'https://yun.tuiapple.com/tuia/dist/image/1.jpg',
                'https://yun.tuiapple.com/tuia/dist/image/2.png',
                'https://yun.tuiapple.com/tuia/dist/image/3.png'
            ])
    </script>
    <!-- 利用bootstrap作为测试css,有2个 -->
    <link rel="stylesheet"
			href="https://yun.tuiapple.com/tuia/dist/css/bootstrap1.css">
    <link rel="stylesheet"
			href="https://yun.tuiapple.com/tuia/dist/css/bootstrap2.css">
</head>

<body>
    <!-- 利用vue作为测试js,有6个 -->
    <script src="https://yun.tuiapple.com/tuia/dist/js/vue1.js"></script>
    <script src="https://yun.tuiapple.com/tuia/dist/js/vue2.js"></script>
    <script src="https://yun.tuiapple.com/tuia/dist/js/vue3.js"></script>
    <script src="https://yun.tuiapple.com/tuia/dist/js/vue4.js"></script>
    <script src="https://yun.tuiapple.com/tuia/dist/js/vue5.js"></script>
    <script src="https://yun.tuiapple.com/tuia/dist/js/vue6.js"></script>
</body>
</html>

我们发现图片几乎是和CSS、JS同时发起下载的。

image.png

最终方案

因为我们的业务环境是要对接很多媒体,但有一些媒体并不支持Https,因此我们同时需要考虑Http2.0和Http1.0的环境。

在Https支持的情况下,我们使用Http2.0方案;在不支持Https的情况下,我们使用link preload结合图像单域名。方式三约束了前端加载的方式,侵入性较强,暂不做考虑。

关于preload,浏览器经过几年的发展,兼容性没什么大问题,可以参见caniuse

更快的背景图

相对来说背景图还是比较大的,怎么才能让它更快的展示呢?

渐进式

JPEG类型有一种渐进JPEG,可以在网络差的情况下避免完全白屏。

6a0120a85dcdae970b0128776fcab6970c.gif

渐进式jpeg(progressive jpeg)图片及其相关

压缩

我们还可以通过压缩图片,对JPEG、PNG格式进行压缩。我们专门做了一个图片压缩服务,这在后续章节会进行介绍。

image.png

同样一张图片,经过webp压缩过后,会更小。

缓存

我们还可以利用缓存,强制图片进行强缓存,加快第二次。

image.png

可以看到第二次,我们就从浏览器的Cache去取到了图片,都不需要用户请求。

内嵌

通过内联base64“小图”,可以看看这个小demo,具体的构建不再这里赘述,提供一个思路。

〇 简要小结

类似骨架屏的一种实现。

  1. 通过无头浏览器在构建前提前跑一次页面,获取当前DOM结构。
  2. 通过提供一个类css的模版(开发者编码时,在模版中设置好图片、颜色等),通过编译生成一段js(具备加载图片、生成css片段能力),插入html头部。
  3. 结合运行时动态生成的css、提前获取的页面dom结构、加载的图片,一个大致的“骨架图”就呈现了。
  4. 最后对于图片加载做了一些讨论。

还是回应下开头,本文可能存在有些纰漏,希望大家多拍砖、建议,谢谢😘。

image.png

结语

回顾作者往期高赞文章,可能有意想不到的收获!

😘点赞+评论+转发😘,原创一篇不易,求鼓励写更多的文章

扩展阅读

资源加载顺序

浏览器页面资源加载过程与优化

有兴趣可以从源码角度分析

从Chrome源码看浏览器如何加载资源 - 知乎

大家有兴趣看JS的优先级,可以参考

Chrome 中 JavaScript 加载优先级 | FENews

图片压缩

Use Imagemin to compress images