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

〇 前言

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

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

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

image.png

本文可能存在有些纰漏,但愿你们多拍砖、建议,谢谢😘。前端

〇 背景

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

image.png

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

所以针对活动页面的数据状况,咱们去作了测试。从测试数据反映,有些页面加载完成率(专业的能够理解为首屏加载率)偏低,但活动页面上一级入口点击率正常。node

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

image.png

怎么去缩短白屏时间呢?git

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

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

〇 “骨架图”实现

骨架屏基本就是详细页面元素未展示时,把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

相关文章
相关标签/搜索