0%

一、背景

大家都知道 Native app 体验确实很好,下载到手机上之后入口也方便。它也有一些缺点:

  • 开发成本高(ios 和安卓)
  • 软件上线需要审核
  • 版本更新需要将新版本上传到不同的应用商店
  • 想使用一个 app 就必须去下载才能使用,即使是偶尔需要使用一下下

而 web 网页开发成本低,网站更新时上传最新的资源到服务器即可,用手机带的浏览器打开就可以使用。但是除了体验上比 Native app 还是差一些,还有一些明显的缺点

  • 手机桌面入口不够便捷,想要进入一个页面必须要记住它的 url 或者加入书签
  • 没网络就没响应,不具备离线能力
  • 不像 APP 一样能进行消息推送
    那么什么是 PWA 呢?

二、What’s PWA?

PWA 全称 Progressive Web App,即渐进式 WEB 应用。

一个 PWA 应用首先是一个网页, 可以通过 Web 技术编写出一个网页应用. 随后添加上 App Manifest 和 Service Worker 来实现 PWA 的安装和离线等功能
解决了哪些问题?

  • 可以添加至主屏幕,点击主屏幕图标可以实现启动动画以及隐藏地址栏
  • 实现离线缓存功能,即使用户手机没有网络,依然可以使用一些离线功能
  • 实现了消息推送
    它解决了上述提到的问题,这些特性将使得 Web 应用渐进式接近原生 App。

三、PWA 的实现

3.1 Manifest 实现添加至主屏幕

index.html

1
2
3
4
5
6
7
<head>
<title>Minimal PWA</title>
<meta name="viewport" content="width=device-width, user-scalable=no" />
<link rel="manifest" href="manifest.json" />
<link rel="stylesheet" type="text/css" href="main.css">
<link rel="icon" href="/e.png" type="image/png" />
</head>

manifest.json

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
{
"name": "Minimal PWA", // 必填 显示的插件名称
"short_name": "PWA Demo", // 可选 在APP launcher和新的tab页显示,如果没有设置,则使用name
"description": "The app that helps you understand PWA", //用于描述应用
"display": "standalone", // 定义开发人员对Web应用程序的首选显示模式。standalone模式会有单独的
"start_url": "/", // 应用启动时的url
"theme_color": "#313131", // 桌面图标的背景色
"background_color": "#313131", // 为web应用程序预定义的背景颜色。在启动web应用程序和加载应用程序的内容之间创建了一个平滑的过渡。
"icons": [ // 桌面图标,是一个数组
{
"src": "icon/lowres.webp",
"sizes": "48x48", // 以空格分隔的图片尺寸
"type": "image/webp" // 帮助userAgent快速排除不支持的类型
},
{
"src": "icon/lowres",
"sizes": "48x48"
},
{
"src": "icon/hd_hi.ico",
"sizes": "72x72 96x96 128x128 256x256"
},
{
"src": "icon/hd_hi.svg",
"sizes": "72x72"
}
]
}

Manifest 参考文档:https://developer.mozilla.org/zh-CN/docs/Web/Manifest
可以打开网站 https://developers.google.cn/web/showcase/2015/chrome-dev-summit 查看添加至主屏幕的动图。

如果用的是安卓手机,可以下载 chrome 浏览器自己操作看看

3.2 service worker 实现离线缓存

3.2.1 什么是 service worker

Service Worker 是 Chrome 团队提出和力推的一个 WEB API,用于给 web 应用提供高级的可持续的后台处理能力。

image

Service Workers 就像介于服务器和网页之间的拦截器,能够拦截进出的 HTTP 请求,从而完全控制你的网站。

最主要的特点

  • 在页面中注册并安装成功后,运行于浏览器后台,不受页面刷新的影响,可以监听和截拦作用域范围内所有页面的 HTTP 请求。
  • 网站必须使用 HTTPS。除了使用本地开发环境调试时(如域名使用 localhost)
  • 运行于浏览器后台,可以控制打开的作用域范围下所有的页面请求
  • 单独的作用域范围,单独的运行环境和执行线程
  • 不能操作页面 DOM。但可以通过事件机制来处理
  • 事件驱动型服务线程

    为什么要求网站必须是 HTTPS 的,大概是因为 service worker 权限太大能拦截所有页面的请求吧,如果 http 的网站安装 service worker 很容易被攻击

浏览器支持情况

image

浏览器支持情况详见: https://caniuse.com/#feat=serviceworkers

生命周期

image

当用户首次导航至 URL 时,服务器会返回响应的网页。

  • 第 1 步:当你调用 register() 函数时, Service Worker 开始下载。
  • 第 2 步:在注册过程中,浏览器会下载、解析并执行 Service Worker ()。如果在此步骤中出现任何错误,register() 返回的 promise 都会执行 reject 操作,并且 Service Worker 会被废弃。
  • 第 3 步:一旦 Service Worker 成功执行了,install 事件就会激活
  • 第 4 步:安装完成,Service Worker 便会激活,并控制在其范围内的一切。如果生命周期中的所有事件都成功了,Service Worker 便已准备就绪,随时可以使用了!

chrome://serviceworker-internals 来了解当前浏览器中所有已安装 Service Worker 的详细情况

3.2.2 HTTP 缓存与 service worker 缓存

HTTP 缓存
Web 服务器可以使用 Expires 首部来通知 Web 客户端,它可以使用资源的当前副本,直到指定的“过期时间”。反过来,浏览器可以缓存此资源,并且只有在有效期满后才会再次检查新版本。
使用 HTTP 缓存意味着你要依赖服务器来告诉你何时缓存资源和何时过期。

service worker 缓存

Service Workers 的强大在于它们拦截 HTTP 请求的能力
进入任何传入的 HTTP 请求,并决定想要如何响应。在你的 Service Worker 中,可以编写逻辑来决定想要缓存的资源,以及需要满足什么条件和资源需要缓存多久。一切尽归你掌控!

3.2.3 实现离线缓存

index.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Hello Caching World!</title>
</head>
<body>
<!-- Image -->
<img src="/images/hello.png" />
<!-- JavaScript -->
<script async src="/js/script.js"></script>
<script>
// 注册 service worker
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/service-worker.js', {scope: '/'}).then(function (registration) {
// 注册成功
console.log('ServiceWorker registration successful with scope: ', registration.scope);
}).catch(function (err) {
// 注册失败 :(
console.log('ServiceWorker registration failed: ', err);
});
}
</script>
</body>
</html>

注:Service Worker 的注册路径决定了其 scope 默认作用页面的范围。
如果 service-worker.js 是在 /sw/ 页面路径下,这使得该 Service Worker 默认只会收到 页面/sw/ 路径下的 fetch 事件。
如果存放在网站的根路径下,则将会收到该网站的所有 fetch 事件。
如果希望改变它的作用域,可在第二个参数设置 scope 范围。示例中将其改为了根目录,即对整个站点生效。

service-worker.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
var cacheName = 'helloWorld';     // 缓存的名称
// install 事件,它发生在浏览器安装并注册 Service Worker 时
self.addEventListener('install', event => {
/* event.waitUtil 用于在安装成功之前执行一些预装逻辑
但是建议只做一些轻量级和非常重要资源的缓存,减少安装失败的概率
安装成功后 ServiceWorker 状态会从 installing 变为 installed */
event.waitUntil(
caches.open(cacheName)
.then(cache => cache.addAll([ // 如果所有的文件都成功缓存了,便会安装完成。如果任何文件下载失败了,那么安装过程也会随之失败。
'/js/script.js',
'/images/hello.png'
]))
);
});

/**
为 fetch 事件添加一个事件监听器。接下来,使用 caches.match() 函数来检查传入的请求 URL 是否匹配当前缓存中存在的任何内容。如果存在的话,返回缓存的资源。
如果资源并不存在于缓存当中,通过网络来获取资源,并将获取到的资源添加到缓存中。
*/
self.addEventListener('fetch', function (event) {
event.respondWith(
caches.match(event.request)
.then(function (response) {
if (response) {
return response;
}
var requestToCache = event.request.clone(); //
return fetch(requestToCache).then(
function (response) {
if (!response || response.status !== 200) {
return response;
}
var responseToCache = response.clone();
caches.open(cacheName)
.then(function (cache) {
cache.put(requestToCache, responseToCache);
});
return response;
})
);
});

注:为什么用 request.clone()和 response.clone()
需要这么做是因为 request 和 response 是一个流,它只能消耗一次。因为我们已经通过缓存消耗了一次,然后发起 HTTP 请求还要再消耗一次,所以我们需要在此时克隆请求
Clone the request—a request is a stream and can only be consumed once.

3.2.4 调试相关

chrome 浏览器打开 https://googlechrome.github.io/samples/service-worker/basic/index.html ,这是一个实现了 service worker 离线缓存功能的网站,打开调试工具

image

介绍一个图中的 1.和 2.

  1. 勾选可以模拟网站离线情况,勾选后 network 会有一个黄色警告图标,该网站已经离线。此时刷新页面,页面仍然能够正常显示
  2. 当前 service worker 的 scope。它能够拦截 https://googlechrome.github.i…,同样也能够拦截https://googlechrome.github.i...下的请求

调试面板具体代表的什么参看 https://x5.tencent.com/tbs/guide/serviceworker.html 的第三部分

3.3 service worker 实现消息推送

image

  • 步骤一、提示用户并获得他们的订阅详细信息
  • 步骤二、将这些详细信息保存在服务器上
  • 步骤三、在需要时发送任何消息

    不同浏览器需要用不同的推送消息服务器。以 Chrome 上使用 Google Cloud Messaging 作为推送服务为例,第一步是注册 applicationServerKey(通过 GCM 注册获取),并在页面上进行订阅或发起订阅。每一个会话会有一个独立的端点(endpoint),订阅对象的属性(PushSubscription.endpoint) 即为端点值。将端点发送给服务器后,服务器用这一值来发送消息给会话的激活的 Service Worker (通过 GCM 与浏览器客户端沟通)。

步骤一和步骤二
index.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Progressive Times</title>
<link rel="manifest" href="/manifest.json">
</head>
<body>
<script>
var endpoint;
var key;
var authSecret;
var vapidPublicKey = 'BAyb_WgaR0L0pODaR7wWkxJi__tWbM1MPBymyRDFEGjtDCWeRYS9EF7yGoCHLdHJi6hikYdg4MuYaK0XoD0qnoY';
// 方法很复杂,但是可以不用具体看,知识用来转化vapidPublicKey用
function urlBase64ToUint8Array(base64String) {
const padding = '='.repeat((4 - base64String.length % 4) % 4);
const base64 = (base64String + padding)
.replace(/\-/g, '+')
.replace(/_/g, '/');
const rawData = window.atob(base64);
const outputArray = new Uint8Array(rawData.length);
for (let i = 0; i < rawData.length; ++i) {
outputArray[i] = rawData.charCodeAt(i);
}
return outputArray;
}
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('sw.js').then(function (registration) {
return registration.pushManager.getSubscription()
.then(function (subscription) {
if (subscription) {
return;
}
return registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: urlBase64ToUint8Array(vapidPublicKey)
})
.then(function (subscription) {
var rawKey = subscription.getKey ? subscription.getKey('p256dh') : '';
key = rawKey ? btoa(String.fromCharCode.apply(null, new Uint8Array(rawKey))) : '';
var rawAuthSecret = subscription.getKey ? subscription.getKey('auth') : '';
authSecret = rawAuthSecret ?
btoa(String.fromCharCode.apply(null, new Uint8Array(rawAuthSecret))) : '';
endpoint = subscription.endpoint;
return fetch('./register', {
method: 'post',
headers: new Headers({
'content-type': 'application/json'
}),
body: JSON.stringify({
endpoint: subscription.endpoint,
key: key,
authSecret: authSecret,
}),
});
});
});
}).catch(function (err) {
// 注册失败 :(
console.log('ServiceWorker registration failed: ', err);
});
}
</script>
</body>
</html>

步骤三 服务器发送消息给 service worker

app.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
const webpush = require('web-push');
const express = require('express');
var bodyParser = require('body-parser');
const app = express();
webpush.setVapidDetails(
'mailto:contact@deanhume.com',
'BAyb_WgaR0L0pODaR7wWkxJi__tWbM1MPBymyRDFEGjtDCWeRYS9EF7yGoCHLdHJi6hikYdg4MuYaK0XoD0qnoY',
'p6YVD7t8HkABoez1CvVJ5bl7BnEdKUu5bSyVjyxMBh0'
);
app.post('/register', function (req, res) {
var endpoint = req.body.endpoint;
saveRegistrationDetails(endpoint, key, authSecret);
const pushSubscription = {
endpoint: req.body.endpoint,
keys: {
auth: req.body.authSecret,
p256dh: req.body.key
}
};
var body = 'Thank you for registering';
var iconUrl = 'https://example.com/images/homescreen.png';
// 发送 Web 推送消息
webpush.sendNotification(pushSubscription,
JSON.stringify({
msg: body,
url: 'http://localhost:3111/',
icon: iconUrl
}))
.then(result => res.sendStatus(201))
.catch(err => {
console.log(err);
});
});
app.listen(3111, function () {
console.log('Web push app listening on port 3111!')
});

service worker 监听 push 事件,将通知详情推送给用户

service-worker.js

1
2
3
4
5
6
7
8
9
10
11
12
13
self.addEventListener('push', function (event) {
// 检查服务端是否发来了任何有效载荷数据
var payload = event.data ? JSON.parse(event.data.text()) : 'no payload';
var title = 'Progressive Times';
event.waitUntil(
// 使用提供的信息来显示 Web 推送通知
self.registration.showNotification(title, {
body: payload.msg,
url: payload.url,
icon: payload.icon
})
);
});

扩展知识:service worker 的更新

总结

PWA 的优势

  • 可以将 app 的快捷方式放置到桌面上,全屏运行,与原生 app 无异
  • 能够在各种网络环境下使用,包括网络差和断网条件下,不会显示 undefined
  • 推送消息的能力
  • 其本质是一个网页,没有原生 app 的各种启动条件,快速响应用户指令

PWA 存在的问题

  • 支持率不高:现在 ios 手机端不支持 pwa,IE 也暂时不支持
  • Chrome 在中国桌面版占有率还是不错的,安卓移动端上的占有率却很低
  • 各大厂商还未明确支持 pwa
  • 依赖的 GCM 服务在国内无法使用
  • 微信小程序的竞争

尽管有上述的一些缺点,PWA 技术仍然有很多可以使用的点。

  • service worker 技术实现离线缓存,可以将一些不经常更改的静态文件放到缓存中,提升用户体验。
  • service worker 实现消息推送,使用浏览器推送功能,吸引用户
  • 渐进式开发,尽管一些浏览器暂时不支持,可以利用上述技术给使用支持浏览器的用户带来更好的体验。

简介

什么是 web worker 呢?从名字上就可以看出,web worker 就是在 web 应用程序中使用的 worker。这个 worker 是独立于 web 主线程的,在后台运行的线程。

web worker 的优点就是可以将工作交给独立的其他线程去做,这样就不会阻塞主线程。

Web Workers 的基本概念和使用

web workers 是通过使用 Worker()来创建的。

Worker 可以指定后台执行的脚本,并在脚本执行完毕之后通常 creator。

worker 有一个构造函数如下:

1
Worker("path/to/worker/script")

我们传入要执行脚本的路径,即可创建 worker。

Workers 中也可以创建新的 Workers,前提是这些 worker 都是同一个 origin。

我们看一下 worker 的定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
interface Worker extends EventTarget, AbstractWorker {
onmessage: ((this: Worker, ev: MessageEvent) => any) | null;
onmessageerror: ((this: Worker, ev: MessageEvent) => any) | null;

postMessage(message: any, transfer: Transferable[]): void;
postMessage(message: any, options?: PostMessageOptions): void;

terminate(): void;
addEventListener<K extends keyof WorkerEventMap>(type: K, listener: (this: Worker, ev: WorkerEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void;
removeEventListener<K extends keyof WorkerEventMap>(type: K, listener: (this: Worker, ev: WorkerEventMap[K]) => any, options?: boolean | EventListenerOptions): void;
removeEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void;
}

declare var Worker: {
prototype: Worker;
new(stringUrl: string | URL, options?: WorkerOptions): Worker;
};

可以看到 Worker 的构造函数可以传入两个参数,第一个参数可以是 string 也可以是 URL,表示要执行的脚本路径。

第二个参数是 WorkerOptions 选项,表示 worker 的类型,名字和权限相关的选项。

1
2
3
4
5
interface WorkerOptions {
credentials?: RequestCredentials;
name?: string;
type?: WorkerType;
}

除此之外,worker 可以监听 onmessage 和 onmessageerror 两个事件。

提供了两个方法:postMessage 和 terminate。

worker 和主线程都可以通过 postMessage 来给对方发送消息,也可以用 onmessage 来接收对方发送的消息。

还可以添加和移除 EventListener。

我们看一个使用 worker 的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
const first = document.querySelector('#number1');
const second = document.querySelector('#number2');

const result = document.querySelector('.result');

if (window.Worker) {
const myWorker = new Worker("worker.js");

first.onchange = function() {
myWorker.postMessage([first.value, second.value]);
console.log('Message posted to worker');
}

second.onchange = function() {
myWorker.postMessage([first.value, second.value]);
console.log('Message posted to worker');
}

myWorker.onmessage = function(e) {
result.textContent = e.data;
console.log('Message received from worker');
}
} else {
console.log('Your browser doesn\'t support web workers.')
}

上面的例子创建了一个 woker,并向 worker post 了一个消息。

再看一下 worker.js 的内容是怎么样的:

1
2
3
4
5
6
7
8
9
10
11
onmessage = function(e) {
console.log('Worker: Message received from main script');
const result = e.data[0] * e.data[1];
if (isNaN(result)) {
postMessage('Please write two numbers');
} else {
const workerResult = 'Result: ' + result;
console.log('Worker: Posting message back to main script');
postMessage(workerResult);
}
}

我们在主线程中向 worker postmessage,在 worker 中通过 onmessage 监听消息,然后又在 worker 中 post message,可以在 main 线程中通过 onmessage 来监听 woker 发送的消息。

这样就做到了一次完美的交互。

再看一下 worker 的兼容性:

image

可以看到,基本上所有的浏览器都支持 worker,不过有些浏览器只支持部分的方法。

如果想要立马结束一个 worker,我们可以使用 terminate:

1
myWorker.terminate();

要想处理 worker 的异常,可以使用 onerror 来处理异常。

如果 worker 的 script 比较复杂,需要用到其他的 script 文件,我们可以使用 importScripts 来导入其他的脚本:

1
2
3
4
importScripts();                         /* imports nothing */
importScripts('foo.js'); /* imports just "foo.js" */
importScripts('foo.js', 'bar.js'); /* imports two scripts */
importScripts('//example.com/hello.js'); /* You can import scripts from other origins */

Web Workers 的分类

Web Workers 根据工作环境的不同,可以分为 DedicatedWorker 和 SharedWorker 两种。

DedicatedWorker 的 Worker 只能从创建该 Woker 的脚本中访问,而 SharedWorker 则可以被多个脚本所访问。

上面的例子中我们创建的 worker 就是 DedicatedWorker。

怎么创建 sharedWorker 呢?

1
var myWorker = new SharedWorker('worker.js');

SharedWorker 有一个单独的 SharedWorker 类,和 dedicated worker 不同的是 SharedWorker 是通过 port 对象来进行交互的。

我们看一个 shared worker 的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
var first = document.querySelector('#number1');
var second = document.querySelector('#number2');

var result1 = document.querySelector('.result1');

if (!!window.SharedWorker) {
var myWorker = new SharedWorker("worker.js");

first.onchange = function() {
myWorker.port.postMessage([first.value, second.value]);
console.log('Message posted to worker');
}

second.onchange = function() {
myWorker.port.postMessage([first.value, second.value]);
console.log('Message posted to worker');
}

myWorker.port.onmessage = function(e) {
result1.textContent = e.data;
console.log('Message received from worker');
console.log(e.lastEventId);
}
}

所有的 postMessage 和 onmessage 都是基于 myWorker.port 来的。

再看一下 worker 的代码:

1
2
3
4
5
6
7
8
9
onconnect = function(e) {
var port = e.ports[0];

port.onmessage = function(e) {
var workerResult = 'Result: ' + (e.data[0] * e.data[1]);
port.postMessage(workerResult);
}

}

worker 也是通过 port 来进行通信。

这里我们使用了 onconnect 用来监听父线程的 onmessage 事件或者 start 事件,这两种事件都可以启动一个 SharedWorker。

再看一下 sharedWorker 的浏览器兼容性:

image

可以看到,比 worker 的兼容性要低很多,只有部分浏览器才支持这个高级特性。

worker 和 main thread 之间的数据传输

我们知道 worker 和 main thread 之间是通过 postMessage 和 onMessage 进行交互的。这里面涉及到了数据传输的问题。

实际上数据在 worker 和 main thread 之间是以拷贝的方式并且是以序列化的形式进行传输的。

前言

Performance 一个在前端开发领域中,无法被忽视的存在。使用 Chrome DevTools 的 performance 面板可以记录和分析页面在运行时的所有活动。本文将详细介绍如何使用 performance 面板解决性能瓶颈。

一、Performance 工具优点

1
2
3
4
5
6
7
- 可视化图形界面
- 每毫秒做的事情
- 文件的执行加载的顺序
- 每毫秒界面展示的效果
- 每个方法执行的顺序和时间(由下至上)
- 倒置的事件火焰图(由下至上)
- 数据总结

二、熟悉 Performance 面板

image

三、工具栏

【3.1】录制: 点击 Record (按 Ctrl+E),这时候 Devtools 就开始录制各种性能指标。记录时,Record 按钮会变成红色。然后按 Record 按钮或再次键入键盘快捷键停止记录;

【3.2】刷新: 刷新页面分析;

【3.3】清除: 清除页面分析结果;

【3.4】上下箭头: 用来上传和下载每一次性能检测报告;

【3.5】Screendshots: 显示屏幕快照,是用来查看在每个时间段界面的变化;

【3.6】Memory: 存储调用栈的大小,在不同时间段的不同大小;

【3.7】Disable Javascript samples: 禁用 JavaScript 调用栈;

【3.8】Enable advanced paint instrumentation(slow): 记录渲染事件的细节;

【3.9】Network: 模拟不同的网络环境;

【3.10】CPU: 模拟不同的 CPU 运行速度;

四、overview(页面性能高级汇总)

这里最主要是整体的界面渲染的时候,每个时间段执行的事件顺序,我们就能知道我们每个时间段(精确到毫秒)都做了什么,当鼠标放上去的时候,我们还可以大图的形式去查看我们每个时间段界面的渲染情况:

【4.1】FPS: 全称 Frames Per Second,表示每秒传输帧数,是速度单位,用来分析动画的一个主要性能指标。如果能够达到 >=60fps(帧)/s 的刷新频率,就可以避免出现卡顿。能保持在 60 的 FPS 的话,那么用户体验就是不错的。

为什么是 60fps?

我们的目标是保证页面要有高于每秒 60fps(帧)的刷新频率,这和目前大多数显示器的刷新率相吻合(60Hz)。如果网页动画能够做到每秒 60 帧,就会跟显示器同步刷新,达到最佳的视觉效果。这意味着,一秒之内进行 60 次重新渲染,每次重新渲染的时间不能超过 1/60=0.01666s(秒), 0.01666s*1000=16.66ms(毫秒)。

不同帧的体验
帧率能够达到 50 ~ 60 FPS 的动画将会相当流畅,让人倍感舒适;
帧率在 30 ~ 50 FPS 之间的动画,因各人敏感程度不同,舒适度因人而异;
帧率在 30 FPS 以下的动画,让人感觉到明显的卡顿和不适感;
帧率波动很大的动画,亦会使人感觉到卡顿。

如下图所示,绿色的长条越高,说明 FPS 越高,用户体验越好。 如果你发现了一个红色的长条,那么就说明这些帧存在严重问题,有可能导致非常差的用户体验。

image

【4.2】CPU: CPU 资源。CPU 图表中的各种颜色代表着在这个时间段内,CPU 在各种处理上所花费的时间。如果你看到了某个处理占用了大量的时间,那么这可能就是一个可以找到性能瓶颈的线索。

**下图中颜色分别为(与 Summary 面板里的颜色是相互对应的)**:

蓝色(Loading): 表示网络通信和 HTML 解析时间
黄色(Scripting): 表示 JavaScript 执行时间
紫色(Rendering): 表示样式计算和布局(重排)时间
绿色(Painting): 表示重绘时间
灰色(other): 表示其它事件花费的时间
白色(Idle): 表示空闲时间

image

【4.3】NET: 每条彩色横杠表示一种资源。横杠越长,检索资源所需的时间越长。 每个横杠的浅色部分表示等待时间(从请求资源到第一个字节下载完成的时间)。

image

【4.4】HEAP: JavaScript 执行的时间分布。

image

五、火焰图(CPU 堆叠追踪的可视化)

image

【5.1】Network: 表示每个服务器资源的加载情况,什么时间加载了什么资源,通过这里,我们更直观的可以知道,资源是并行加载的

【5.2】Frames: 表示每幅帧的运行情况

【5.3】Timings: 上图中有 4 条虚线,分别表示如下:

  • DCL(DOMContentLoaded): 表示 HTML 文档加载完成事件。当初始 HTML 文档完全加载并解析之后触发,无需等待样式、图片、子 frame 结束。作为明显的对比,load 事件是当个页面完全被加载时才触发
  • FP(First Paint): 首屏绘制,页面刚开始渲染的时间
  • FCP(First Contentful Paint): 首屏内容绘制,首次绘制任何文本,图像,非空白 canvas 或 SVG 的时间点
  • FMP(First Meaningful Paint): 首屏有意义的内容绘制,这个“有意义”没有权威的规定,本质上是通过一种算法来猜测某个时间点可能是 FMP。有的理解为是最大元素绘制的时间,即同 LCP(Largest Contentful Paint)
  • L(Onload): 页面所有资源加载完成事件。
  • LCP(Largest Contentful Paint): 最大内容绘制,页面上尺寸最大的元素绘制时间。
    其中 FP、FCP、FMP 是同一条虚线,三者时间不一致。比如首次渲染过后,有可能出现 JS 阻塞,这种情况下 FCP 就会大于 FP

【5.4】Main: 表示主线程

主要负责

  • Javascript 的计算与执行
  • CSS 样式计算
  • Layout 布局计算
  • 将页面元素绘制成位图(paint),也就是光栅化(Raster)
  • 将位图给合成线程

【5.5】Raster: 光栅化(处理光栅图,即位图)

【5.6】GPU: 表示 GPU 占用情况

【5.7】Chrome_childIOThread: 子线程

【5.8】Compositor: 合成线程

主要负责

  • 将位图(GraphicsLayer 层)以纹理(texture)的形式上传给 GPU
  • 计算页面的可见部分和即将可见部分(滚动)
  • CSS 动画处理
  • 通知 GPU 绘制位图到屏幕上

【5.9】Memory: 上面有提到 Memory 选项,在勾选后,就会显示该事件折线图,通过该图,可以看出我们在不同的时间段,不同事件的执行情况

  • JS Heap: 表示 JS 占用的内存大小。
  • Documents: 表示文档数。
  • Nodes: 表示 Node 节点数
  • Listeners: 表示监听数。
  • GPU Memory: 表示 GPU 占用数
    4 条折线图是以上 4 个指标(没有 GPU 消耗)对应的时间消耗的内存大小与节点数量。若将某项指标前面的勾选去掉则不会出现对应的折线。注意这个折线图只有在点击 Main 主线程的时候才会有,选择其他的指标时折线图区域是空白。

六、统计汇总(以图表的形式汇总数据)

image

【6.1】Summary: 表示各指标时间占用统计报表

  • Loading: 加载时间
  • Scripting: js 计算时间
  • Rendering: 渲染时间
  • Painting: 绘制时间
  • Other: 其他时间
  • Idle: 浏览器闲置时间

【6.2】Bottom-Up: 表示事件时长排序列表(倒序)

这里和 Main 里面看见的,其实是一个对应着的关系,从这里,我们可以看见所有的事件列表,还有每个事件的 Self Time(自己调用的时间) 、Total Time(总调用时间,包括子项调用时间) 、Activity(行为,包括调用该事件的位置)

image

【6.3】Call Tree: 表示事件调用顺序列表

其实这里和 Bottom-Up 部分是一样的,就不做太多的说明了

image

【6.4】Event Log: 表示事件发生的顺序列表

这里比前面的 Bottom-Up 和 Call Tree 相比,多了一个 Start Time 属性,这个属性其实就是开始的时间,从什么时间开始执行的什么事件

image