网站加载性能

参考文档为google developers中web性能部分的加载性能章节。大家可以不看这篇文章,直接去看官网文档。
官网地址
本文为一些总结和个人观点。

目的

目的当然是快。对于快,官方也给予了定义(RAIL),满足这个要求即可,避免过度优化。
RAIL 是一种以用户为中心的性能模型。是Response、Animation、Idle、Load四个单词的缩写。

  • 以用户为中心;最终目标不是让您的网站在任何特定设备上都能运行很快,而是使用户满意。
  • Response 立即响应用户;在 100 毫秒以内确认用户输入。
  • Animation 设置动画或滚动时,在 10 毫秒以内生成帧。
  • Idle 最大程度增加主线程的空闲时间。
  • Load 持续吸引用户;在 1000 毫秒以内呈现交互内容。

本文只涉及Load部分:如果网站可以秒开,那就完美了。

关键渲染路径 CRP(Critical Rendering Path)

网站加载性能跟这个概念密切相关,这个概念是指当浏览器接收到html后,到完成第一次绘制到屏幕所需要的步骤。原文地址

  1. 构建DOM树(Constructing the DOM Tree)
  2. 构建CSSOM树 (Constructing the CSSOM Tree)
  3. 运行JS (Running JavaScript)
  4. 创建渲染树 (Creating the Render Tree)
  5. 生成布局 (Generating the Layout)
  6. 绘制 (Painting)
image.png

1. 构建DOM树

DOM(Document Object Model)树包含了页面的所有节点的对象。
HTML可以被部分执行,意思是不需要完整地构建完DOM树才展示到页面,可以一部分一部分展示。但是,CSS和JS会阻塞这个过程。

2. 构建CSSOM树

CSSOM(CSS Object Model)是包含了与DOM相关的样式对象。和DOM相似,但是包含了所有节点的样式信息,不管是明确声明的还是继承的(附:原文没提,还有浏览器默认的)。
CSS是阻塞渲染的资源,意思是在CSS资源被解析前,渲染树不会被创建。这是因为CSS的级联特性、后面加载的样式会覆盖前面的。
CSS同样会阻塞脚本执行,因为JS文件必须得等CSSOM构建完才能运行。

3. 运行JS

JS是阻塞解析(HTML)的资源,意思是解析HTML文档会被JS阻塞。
当解析器遇到script标签,会停止解析,去获取执行JS。

4. 创建渲染树

通过结合DOM和CSSOM,创建最终显示在页面上的渲染树(例如display为none的元素,就不会出现在渲染树中)。

5. 生成布局

这个我没看太懂,讲的是viewport大小,意思是整个视窗的大小,作为CSS样式的上下文。跟平时讲的layout有所不同。

6. 绘制

将可见元素(附:渲染树)转换成像素显示到屏幕上。
绘制的时间取决于DOM和样式的复杂程度。

文章最后的调试图片跟目前Chrome DevTools显示出入比较大,但是挨个展开event log中的task,基本是一致的。

测试工具

  • Lighthouse
  • PageSpeed Insights
  • WebPageTest
  • Pingdom

这几个工具都是可以出性能报告的。单个工具都不是十分准确,详尽的,要结合使用。
其中Lighthouse可以直接在DevTools中Audits面板中打开,下图是报告(低性能模式)


Lighthouse报告

其中First Meaningful Paint(FMP)和Time to Interactive(TTI)是两个很重要的指标。

下图是WebPageTest出的报告:

WebPageTest报告

总结

为什么总结要写在这里呢,因为事无巨细,官方文档写得方方面面涉及太多了。数了一下,细节点大概五六百个,所以先总结理清一下思路。

概览

文章最后还介绍了webpack,webpack解决了大部分问题。
下面就按照这几个方面阐述(与官方文档顺序不一样)。

减少资源体积

文本

JS、HTML、CSS文件都属于文本资源。

1. 开发和生产代码分离

现代框架都提供build脚本构建生产版本代码。

2. 最小化

即删除注释、空格、换行等无用的东西。

3.源码压缩

使用UglifyJS压缩ES5代码。使用babel-minify或uglify-es压缩ES2015+代码。

4.传输压缩

即采用Gzip压缩传输文本。Brotli~q11更好。

5.减少库的使用

有些库很大的,譬如jQuery,压缩后也有100K左右,如果只用个选择器,完全可以用浏览器自带的querySelector等方法,或者使用jQuery核心库Sizzle。

6.Tree Shaking(删除未使用代码)

前提是使用ES6模块,webpack在打包时候会删除未使用的代码。
然后在引用代码的时候,按需引用。
例如

import * as utils from "../../utils/utils";

改成

import { simpleSort } from "../../utils/utils";

注意:

  • babel会将ES6模块转成CommonJS模块,要关闭此功能。
{
  "presets": [
    ["env", {
      "modules": false
    }]
  ]
}

特殊情况1(例lodash)

有些包不是按照ES6模块开发的,按照上面格式引用依然无用,譬如lodash

// This still pulls in all of lodash even if everything is configured right.
import { sortBy } from "lodash";

要解决此问题,有两种方式

  • 改用lodash-es
// This will only pull in the sortBy routine.
import sortBy from "lodash-es/sortBy";
  • 安装babel-plugin-lodash插件

特殊情况2 (例moment.js)

moment库压缩后有223KB大小,其中170KB是语言包,不优化的话,会全部被打包到生产环境代码去。
要解决此问题,使用moment-locales-webpack-plugin插件。

其他特殊情况

https://github.com/GoogleChromeLabs/webpack-libs-optimizations
列举了很多特殊情况(包括上面两种)

图片、视频

优化

一堆专业性的压缩图片技术和工具就不讲了,就是减少图片和视频体积(在不影响网页观看效果前提下)。

  • 选择合适图片格式(例:从png 232K 到 jpg 42K)
  • 删除元数据(即一些“无用信息”:相机、拍照日期、位置等,例:363K删到325K)
  • 裁剪图片尺寸;提供展示缩略图;降低图片质量(227K到20K)
  • 压缩图片(551K到133K)

gif动画转视频

gif动画非常大的,可以利用工具转换成视频,并设置自动播放。
例:将一个13.7MB的gif动画转换成了867K的mp4视频。
webm更好,但没有mp4兼容性好。

<video autoplay loop muted playsinline>
  <source src="oneDoesNotSimply.webm" type="video/webm">
  <source src="oneDoesNotSimply.mp4" type="video/mp4">
</video>

根据设备信息请求不同图片

srcset,和媒体查询类似,不同设备请求不同质量图片

<picture>
  <source media="(min-width: 800px)" srcset="head.jpg, head-2x.jpg 2x">
  <source media="(min-width: 450px)" srcset="head-small.jpg, head-small-2x.jpg 2x">
  <img src="head-fb.jpg" srcset="head-fb-2x.jpg 2x" alt="a head carved out of wood">
</picture>

图像CDN

专业图像CDN站会帮你自动处理好图片,但收费。

字体

字体的优化也讲了很多,有格式问题,有Gzip压缩问题。
值得注意的是 unicode-range 描述符,可以指定加载部分子集(不知道要不要服务器支持)。
例如,您可以将 Awesome Font 系列拆分成拉丁文和日文子集,其中的每个子集将由浏览器根据需要下载:

@font-face {
  font-family:'Awesome Font';
  font-style: normal;
  font-weight: 400;
  src: local('Awesome Font'),
       url('/fonts/awesome-l.woff2') format('woff2'),
       url('/fonts/awesome-l.woff') format('woff'),
       url('/fonts/awesome-l.ttf') format('truetype'),
       url('/fonts/awesome-l.eot') format('embedded-opentype');
  unicode-range:U+000-5FF; /* Latin glyphs */
}

@font-face {
  font-family:'Awesome Font';
  font-style: normal;
  font-weight: 400;
  src: local('Awesome Font'),
       url('/fonts/awesome-jp.woff2') format('woff2'),
       url('/fonts/awesome-jp.woff') format('woff'),
       url('/fonts/awesome-jp.ttf') format('truetype'),
       url('/fonts/awesome-jp.eot') format('embedded-opentype');
  unicode-range:U+3000-9FFF, U+ff??; /* Japanese glyphs */
}

减少请求次数

这里的减少请求次数是指减少网络请求次数,所以缓存也算在这个优化里。

原因

1. 一个请求的生命周期

请求生命周期

可以看到,一个请求要经过队列等待、DNS查找、初始化连接、SSL、发送请求、等待、下载等过程。
每一步都是需要时间和资源的。

2. Queueing(队列)

  • 存在更高优先级的请求
  • 此源已打开六个 TCP 连接,达到限值。 仅适用于 HTTP/1.0 和 HTTP/1.1
  • 浏览器正在短暂分配磁盘缓存中的空间
    这里的优先级需要讲一下,浏览器拥有自己的启发式算法,自动为各个资源评级,等级高的会优先去获取资源,低的会等待。看下图Priority列


    请求优先级

    浏览器用自己的启发式算法为各种资源分配了默认级别:

  • 给了html highest级别,这是肯定的
  • 给了css文件highest级别,这是因为css会阻碍渲染(后面会讲)
  • 然后给了js文件high级别(图中有一些low的js文件是懒加载的)
  • 其他一些不重要的资源例如图片都给了low级别

3. 网络连接

http连接是基于TCP的,关于http和TCP及网络带宽、延迟对加载网页影响,可以看下面这篇文章。
Primer on Web Performance
我的个人理解是,tcp连接是一个复杂的过程,要消耗性能和资源,而http/1的底层设计更是让这个过程很耗时,多个请求要建立多次tcp连接(关于http/2是如何优化的后面会讲)。而且建立一个tcp连接后,会根据资源的大小,往返多次。

4. 综上所述

请求次数越少越好,力求主页加载的都是自己所需要的资源。

合并文本资源

就是将多个css文件合并成一个,多个js文件合并成一个,webpack打包工具就可以做到。

合并图片

即将多张背景图片合并成一张,然后通过背景图片位置显示不同图片。

JS位置

关于如何加载JavaScript,又是一个复杂的,历史性的问题,可以参照这篇文章Deep dive into the murky waters of script loading
其复杂性在于,要想达到如下目的,很麻烦。

  • 不阻塞渲染
  • 不导致重复下载
  • 兼容各浏览器
    简单的解决方案是

1. 将script引用标签放在body底部

2. 如果有js脚本必须先执行且代码较少,可以直接硬写到html中,这样可以减少请求次数

缓存

  • Memory Cache
  • Service Worker Cache
  • HTTP Cache (Disk Cache)
  • Push Cache
    一个请求会按顺序读取这几层缓存,一旦命中就返回资源。
  1. Memory Cache
    不是我们可以控制的,浏览器会缓存一些图片等资源到内存中,再次遇到请求直接返回,一个例外是preload的资源也会放在Memory Cache中。
  2. Service Worker Cache
    可以说给我们开了一个口子,去定制缓存的行为,完成一些用http缓存完全做不到的事情。Service Worker完全没有任何规则,就看我们怎么去编码控制了,自由度极高,缓存的地方也自由:LocalStorage、IndexedDB、FilesSystem、Caches Api。每个存储类型都有各自的特点及使用场景。
    官方列举了一些建议规则及实现代码及其使用场景
    • Cache only (仅缓存)
    • Network only(仅网络)
    • Cache,falling back to network(优先缓存,网络备用)
    • Cache & network race(缓存和网络竞赛)
    • Network falling back to cache(优先网络,缓存备用)
    • Cache then network(优先缓存,同时去请求网络(下次请求可用))
    • Generic fallback(常规回退,譬如自定义一个字符串)
    • ServiceWorker-side templating(模板)
  3. HTTP Cahce(Disk Cache)
    很古老的东西了
  4. Push Cache
    这是结合HTTP/2协议的缓存,简言之就是,请求index.html时候,服务器不仅返回了index.html,也推送了其他关键资源,这些资源被放在Push Cache中。

HTTP/2

HTTP/2协议是好东西,如果有条件使用一定要使用,以二进制分帧为核心从底层解决了HTTP/1的一些令人诟病的问题。使用了HTTP2,上面的一些优化手段可以舍弃了(主要是代码合并方面的,例如级联文件、图片精灵)

1. 连接复用

在 HTTP/1.x 中,如果客户端要想发起多个并行请求以提升性能,则必须使用多个 TCP 连接,不仅耗性能,浪费资源,而且会造成队首阻塞,效率低下。
HTTP/2 利用二进制分帧层解决了这个问题,可以在一个TCP连接中并行发送多个请求和响应,应用速度更快、开发更简单、部署成本更低。

2. 标头压缩

HTTP/2 使用HPACK压缩请求和响应标头(header),极大减少了标头大小(减小85%-88%)。

我们发现,仅仅由于标头压缩,页面加载时间就减少了 45 - 1142 毫秒

3. 服务器推送

一般打开一个网站,是先向服务器请求一个index.html文件,然后解析html,再去加载js、css等资源,这是两个串行过程。
而HTTP/2提供了服务器推送技术,打开一个网站,服务器知道你需要什么资源,一起把index.html及js、css等静态资源全部推送给你,当浏览器解析完html,请求js和css时候,直接在缓存中取就可以了。减少了一次网络请求。
有好处就有弊端:处理的不好的话,重复发送资源,浪费了缓存这一功能。

优化关键渲染路径

关键渲染路径 CRP(Critical Rendering Path)的概念在最开始就讲了。
下面说一下各个过程简介及调试方法(因官网的配图都比较老了)。

文件列表

index.html

<!DOCTYPE html>
<html>
<head>
  <title>Understanding the Critical Rendering Path</title>
  <link rel="stylesheet" href="main.css">
</head>
<body>
  <header>
      <h1>Understanding the Critical Rendering Path</h1>
  </header>
  <main>
      <h2>Introduction</h2>
      <p>Lorem ipsum dolor sit amet</p>
      <div>
        <img src="./images/panda.jpeg" />
      </div>
  </main>
  <footer>
      <small>Copyright 2017</small>
  </footer>
  <script src="main.js"></script>
</body>
</html>

main.css

body { font-size: 18px; }

header { color: plum; }
h1 { font-size: 28px; }

main { color: firebrick; }
h2 { font-size: 20px; }

footer { display: none; }

main.js

function abc() {
  console.log("just a simple js");
}
abc();

panda.jpeg

panda

1.构建对象模型(DOM)

Bytes -> Characters -> Tokens -> Nodes -> DOM
四个过程

  1. 根据编码将字节转换成字符串
  2. 令牌化:将字符串转换成各个html标签
  3. 词法分析:将标签转换成节点对象(包含属性和规则)
  4. 构建DOM树

调试查看(找到Parse HTML)

打开Chrome DevTools -> Audits,评估完点击 View Trace,就会进入Perfermance面板。

Parse Html

主线程Main中的火焰图和Event Log中是一一对应的。
我们一个一个展开EventLog中的Task,找到Parse HTML,看到解析HTML,构建DOM树,话费了1.1ms。

提示:totaltime是这一过程总时间,selftime是自身执行时间,totaltime - selftime就是其调用的子任务的执行时间。

2. 构建CSS对象模型(CSSOM)

Bytes -> Characters -> Tokens -> Nodes -> DOM
与构建DOM类似,不过产出是CSSOM。

小细节:浏览器会有一组默认样式(User Agent),也可以说会有一个默认CSSOM树,解析样式其实是去替换默认样式。

调试查看(找到Parse Stylesheet)

这一个我调试验证了很久,我觉得官网说的是对应DevTools中Recalculate Style过程,并不对,应该是Parse Stylesheet,理由如下:

  • CSSOM构建会阻塞js执行,main.js必须等到CSSOM构建好,才能执行。这一点在Recalculate Style上并不能体现,反而在Parse Stylesheet上得到了证明:main.js百分百在Parse Stylesheet后执行
  • DOM解析构建对应DevTools中的Parse HTML,是蓝色的。Parse Stylesheet也是蓝色的,有理由相信就是CSSOM的解析构建。

官方上这样写,可能是之前的DevTools并没有如此细分。网上查不到相关信息,所以个人理解可能也不对。

Parse Stylesheet

3. 构建渲染树(Render tree)

前面根据HTML构建了完整的DOM树、根据CSS构建了完整的CSSOM树。
这是两个独立的树,根据这两个树,合并计算出渲染树。
这个过程大概是这样:

  1. 遍历DOM树中可见节点(不可见节点有:script、meta标签等;display:none的元素等,注意visibiility:hidden的不同,会依然占据布局)
  2. 对每个可见节点,找到匹配的CSSOM规则并应用。
  3. 发射可见节点,连同其内容和计算的样式(我的理解就是发射渲染树)

调试查看(找到Recalculate Style)

同上,跟官网有出入。而且我认为构建渲染树对应Recalculate Style理由如下:

  • 如果在main.js中调用getComputedStyle获取样式,会强制提前执行Recalculate Style过程,所以Recalculate Style应该就是构建渲染树。


    Recalculate Style

    Recalculate Style还列出了相关信息:上图是影响了8个元素

4. 布局(layout或者叫reflow)

根据渲染树和视口(viewport),计算所有可见元素相对视口的绝对像素位置和像素尺寸,输出盒模型(box model)。

调试查看(找到Layout)

Layout

Layout也列出了相关信息:上图是需要布局11个元素

5. 绘制(painting或者叫rasterizing)

就是输出到屏幕了

调试查看(找到Update Layer Tree、Paint、Composite Layers、Rasterize Paint)

  • 其中Update Layer Tree我也不知道是归于Layout还是归于painting好。
  • 拿剪贴画为例,要做一个三层的剪贴画,先在一张纸上画上草地,然后剪一个小人并画上衣服,然后剪一个鼻子涂上红色,最后一个个贴上去。Paint就是画画的过程,Composite Layers就是贴的过程,Rasterize Paint就是最后成果图输出到屏幕了。(剪多大,贴在哪儿就是前面Layout的过程了)
  • 注意这里的Rasterize Paint已经不再主线程(Main)里了。
Update Layer Tree、Paint、Composite Layers、Rasterize Paint

优化关键路径

优化关键路径就是缩短1-5消耗的总时间。尽快将网页渲染到屏幕上。还能缩短首次渲染后屏幕刷新的事件,为交互式内容实现更高刷新率(不懂)。

CSS会阻塞渲染

由上可知,同时具有 DOM 和 CSSOM 才能构建渲染树,所以要精简CSS,并尽快提供。而且要用好媒体查询,例如指定打印的css文件就不会阻塞渲染。

验证

为了验证CSS会阻塞渲染,需要让css文件延迟响应,我搭建了一个Go服务,延迟2秒返回css文件。


Go文件目录

wiki.go

package main

import (
    "fmt"
    "io/ioutil"
    "net/http"
    "path/filepath"
    "time"
)

func main() {
    // 延迟css文件
    http.HandleFunc("/main.css", func(w http.ResponseWriter, r *http.Request) {
        time.Sleep(2000 * time.Millisecond)
        // fmt.Fprintf(w, "Hello, you've requested: %s\n", r.URL.Path)
        absPath, _ := filepath.Abs("./static/main.css")
        maincss, _ := ioutil.ReadFile(absPath)
        w.Header().Set("Content-Type", "text/css")
        fmt.Fprintf(w, string(maincss))
    })

    // 其他文件直接读取
    http.Handle("/", http.FileServer(http.Dir("static/")))
    http.ListenAndServe(":80", nil)
}

index.html

<!DOCTYPE html>
<html>
  <head>
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <link href="main.css" rel="stylesheet">
    <title>Critical Path: Script</title>
  </head>
  <body>
    <p>Hello <span>web performance</span> students!</p>
    <footer>footer</footer>
  </body>
</html>

新打开一个浏览器标签页,打开localhost,会发现页面空白2秒后才显示内容。


延迟2秒加载css文件

打开performance调试,会发现有2秒的空白时间在等待css文件,然后才进行执行脚本,页面渲染。

使用JS添加交互

引用:JavaScript 允许我们修改网页的方方面面:内容、样式以及它如何响应用户交互。 不过,JavaScript 也会阻止 DOM 构建和延缓网页渲染。 为了实现最佳性能,可以让您的 JavaScript 异步执行,并去除关键渲染路径中任何不必要的 JavaScript。

    1. JavaScript 可以查询和修改 DOM 与 CSSOM
    1. JavaScript 执行会阻止 CSSOM
    1. 除非将 JavaScript 显式声明为异步,否则它会阻止构建 DOM

1. JavaScript 可以查询和修改 DOM 与 CSSOM

这一条不用讲什么了

2. JavaScript 执行会阻塞 CSSOM

这一句我验证时候绕了很多弯弯,开始我猜测是JavaScript的加载执行结束,才会进行CSSOM构建,多方验证失败(在服务端延迟2秒返回js文件,浏览器会先渲染js之前的,加载执行js后进行第二次渲染)。后来感觉作者的意思应该很直白,就是:JavaScript执行和CSSOM都是在主线程执行的,JavaScript执行时候,CSSOM构建肯定不能执行,就这么简单。
index.html

<!DOCTYPE html>
<html>
  <head>
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <link href="main.css" rel="stylesheet">
    <title>Critical Path: Script</title>
  </head>
  <body>
    <p>Hello <span>web performance</span> students!</p>
    <script>
      var str = "";
      for(let i=0; i < 10000000; i++) {
        str += "a";
      }
    </script>
    <footer>footer</footer>
  </body>
</html>
JS执行会阻塞CSSOM

可以看到,我写了一个执行长达941ms的脚本,在这段脚本执行结束后,才进行了CSSOM构建。

3. 除非将 JavaScript 显式声明为异步,否则它会阻止构建 DOM。

这个意思是,html中遇到js,将会停止解析DOM,等待js下载执行完毕继续解析后面的DOM。
在Go服务器中将main.js延迟20ms返回,查看performance。
index.html

<!DOCTYPE html>
<html>
  <head>
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <link href="main.css" rel="stylesheet">
    <title>Critical Path: Script</title>
  </head>
  <body>
    <p>Hello <span>web performance</span> students!</p>
    <script src="main.js"></script>
    <footer>footer</footer>
  </body>
</html>
延迟20ms返回js,无async

可以很明显看到html被分成2段执行了,js后面的页面等待js加载执行完毕才解析。
<script src="main.js"></script>改成<script src="main.js" async></script>

延迟20ms返回js,有async

加上async后,浏览器不等待js文件加载执行,就解析完了dom。

4. JavaScript 执行将暂停,直至 CSSOM 就绪

参照上面的CSS会阻塞渲染配置,JS会等待CSS加载解析完毕,才会执行。
同样在Go服务器将css延迟2秒返回

CSSOM会阻碍JavaScript执行

这里讲几个注意点

  • 4和5肯定是在6之前执行的。6和8其实并不一定严格按顺序执行,如果你在6里读取了元素样式,那么8就会在6之前执行,因为浏览器必须计算完样式(Recalculate Stylesheet),js才能获取正确的样式
    例如在main.js中加段代码:
var span = document.getElementsByTagName('span')[0];
span.textContent = 'interactive'; // change DOM text content
var spanStyle = getComputedStyle(span, null);
console.log(spanStyle.display);
js中读取了样式,导致立即计算样式

可以看到,因为js读取了样式,导致了浏览器立即计算样式返回给js。

这个在网站运行性能里有详细讲解,并且如果代码写得不好,会导致反复计算样式,性能奇差。

  • 接上条,如果读取了元素的尺寸相关,还会触发布局(Layout)
    加上代码,读取了元素高度
  var span = document.getElementsByTagName('span')[0];
  console.log(span.clientHeight);
读取了元素尺寸
  • style和getComputedStyle的区别
var span = document.getElementsByTagName('span')[0];
span.textContent = 'interactive'; // change DOM text content
console.log(span.style.display)

这段代码并不会触发浏览器计算样式,猜测是因为这样写(这个属性)是读取DOM属性。

5. 将js声明成异步(async)

<script src="main.js" async></script>

这样就不会阻塞构建DOM了,不过不能保证js的执行顺序,而且各个浏览器实现的不一致。

延迟加载(懒加载)、预加载

大概分了几种情况

  • 针对图片视频,可见范围外的不加载
  • 代码拆分,首屏只加载必须的资源,发生路由跳转时候加载其他资源
  • 首屏加载必须的资源后,主动预加载一些其他资源,优化后续体验

1. 延迟加载(懒加载)图片、视频

初始只加载屏幕内的图片、视频,其他用占位图片代替。待用户操作(滚动页面)到相关位置,再去获取图片。

实现方式

a. 使用Intersection Observer(最优)

b. 使用Intersection Observer polyfill

npm install intersection-observer

c. 使用事件处理程序(兼容性最好的方法)

利用scroll、resize事件;利用getBoundingClientRect方法与orientationchange事件

d. 使用库

CSS中的图片

CSS中的图片有一个特点:浏览会计算需要哪些背景图,才会去加载,所以可以生命两种样式,设置不同的背景图片,通过切换样式来达到延迟加载

视频

视频需要设定preload="none"属性阻止加载视频资源,设定poster="one-does-not-simply-placeholder.jpg"属性来指定占位图片。

视频自动播放

延迟加载视频,又要像gif那样自动播放。
设定autoplay后,通过poster属性设定占位符,data-src设定真实资源,需要时候将src改成data-src。

JavaScript 代码拆分和延迟加载

这主要牵扯到工程化,一般工程化会把单页面应用打包成一个js文件,这样首屏消耗是很高的,可以利用代码拆分让首屏只加载必须的资源。

  • 动态import
onShowCommentsClick(() => {
  import('./comments').then((comments) => {
    comments.renderComments();
  });
});

这样Webpack就会将comments组件单独打包,首屏不会加载,交互时候才加载

  • 单页面应用:按路由拆分代码
    跟动态import类似,不过是路由级别的拆分
  • 多页面应用
    按照Webpack配置不同的入口文件
// webpack.config.js
module.exports = {
  entry: {
    home: './src/Home/index.js',
    article: './src/Article/index.js',
    profile: './src/Profile/index.js'
  },
};

3. 预加载

这主要是优化交互体验了,跟首屏关系不大,预先加载一些即将使用的资源(譬如代码拆分后的首屏外的其他资源),后续操作就不会卡顿了。

资源优先级

上面讲队列的时候稍微提到了下。
浏览器加载资源会有不同优先级,查看方法:DevTools -> Network,右击表格的标题栏,勾选Priority选项:


优先级

浏览器会采用启发式算法,对不同资源有不同的优先级。因为网络连接是有限的,所以会为优先级高的资源请求,剩下的排队等待。
可以通过如下方式改变一些行为:

1. 预加载 preload

<link rel="preload" as="script" href="super-important.js">
<link rel="preload" as="style" href="critical.css">

preload可以提高资源的优先级,使用场景有:字体的加载优先级是很低的,可以通过preload来提高;preload可以加载CSS或者JS中定义使用的资源;html中引用css会阻碍渲染,可以通过preload来避免;

2. 预连接 preconnect

<link rel="preconnect" href="https://example.com">

一个请求要经过很多步骤,preconnect可以预先进行一些步骤:DNS 解析,TLS 协商,TCP 握手。大大改善了一些情况下的延迟问题。
也可以使用dns-prefetch,仅仅进行DNS查询。

3. 预提取 prefetch

这个跟preload很容易弄混淆,prefetch是去预提取一些资源到缓存中,以后会用到。

4. 预渲染 prerender

和prefetch相似,但是prerender会在后台渲染页面。(不知道怎么测试,我测试了好像没什么效果)

页面绘制

这一部分不单单是针对加载页面了,关于js执行效率和页面渲染效率等,打算另开一个文章写。

其他

性能API

Navigation Timing和Resource Timing帮我们收集性能相关信息。
使用这些API有如下特点

  • 数据化:不用再在DevTools中一点点调试了,可以直接读取相关数据,做出自己的性能分析报告(很多性能测试工具就是基于此API);
  • 更准确:打开DevTools调试其实也是消耗性能的,加载比直接打开网页要慢
  • 可以收集用户真实的数据:直接在客户端收集信息后上传服务器,统计分析

Navigation Timing

performance.getEntriesByType("navigation");

导航时间线收集HTML文档性能指标


Navigation Timing

Resource Timing

资源时间线收集了文档依赖的各种资源的性能指标

performance.getEntriesByType("resource");
Resource Timing

main.js的时间线

其他相关用法

  • getEntriesByName:收集某个资源的性能指标
  • getEntries:收集所有性能指标
  • PerformanceObserver:自己循环遍历性能指标,会带来些问题,可以实现这个对象实例来遍历
  • navigator.sendBeacon:如果想将用户性能指标数据发给服务器,可以写在unload事件中,但是一般写法(ajax,fetch等)会阻塞浏览器。可以使用navigator.sendBeacon避免这个问题。

新的时间API:performance.now

相关文档
上面的时间线中的时间都是通过这个方法处理处理的。

背景

之前处理时间一般都是用Date相关方法。有很多缺陷

  1. 不够精确,只能到毫秒数
  2. 不仅不够精确,还不准(受制于系统时钟偏移)
    例如以前自己用Date获取时刻相减少,提示一段代码执行时间,发现,有时候执行16ms,有时候执行60多ms,显然是不对的。(当然也不会偏差的过分)。

所以有了新规范,目前是High Resolution Time Level 2。
具体实现就是通过实现performance相关属性和方法。

使用

performance.now(); // 7219725.014999989

输出了一个相对于初始时间的亚毫秒数(sub-millisecond,其实就是高精度的毫秒数)。

初始时间

可以通过

performance.timeOrigin // 1558493857670.848

获取。这个属性值,可以是浏览上下文创建时间,可以是新文档对象创建时间,可以是worker创建时间。不用关心初始时间,只是个参照而已。性能分析分析的都是时间段。

requestAnimationFrame

以前写动画都是利用setTimeout来处理的,有诸多弊端,性能也不好。这个方法可以改善这个问题。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,686评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,668评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,160评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,736评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,847评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,043评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,129评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,872评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,318评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,645评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,777评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,470评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,126评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,861评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,095评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,589评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,687评论 2 351

推荐阅读更多精彩内容