lighthouse实践及分析

# 背景

# 为什么要关注Web性能

  • Web性能可以直接影响业务指标,例如转化率和用户满意度

# Lighthouse

lighthouse https://m.jd.com --locale zh --quiet --chrome-flags="--headless"  --only-categories=performance  
1

# 性能指标

image-20220216202551550

# FP和FCP

  • First Paint(首次渲染)表示了浏览器从开始请求网站到屏幕渲染第一个像素点的时间
  • First Contentful Paint(首次内容渲染) (opens new window)表示浏览器渲染出第一个内容的时间,这个内容可以是文本、图片或SVG元素等,不包括iframe和白色背景的canvas元素

# 记录FP和FCP

安装依赖

npm install express morgan compression --save
1

site\index.js

const express = require('express');
const logger = require('morgan');
const delayConfig = require('./delayConfig');
const app = express();
app.use(logger('dev'));
app.use((req, res, next) => {
    let url = req.url;
    let delay = delayConfig[url];
    if (delay) {
        setTimeout(next, delay);
    } else {
        next();
    }
});
app.use(express.static('public'));
app.listen(80, () => console.log(`server started at 80`));
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

site\delayConfig.js

const delayConfig = {
    "/index.html": 100
}
module.exports = delayConfig;
1
2
3
4

site\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">
    <title>lighthouse</title>
</head>

<body>
    <canvas style="width:100%;height:500px;"></canvas>
    <div>hellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohello</div>
    <script src="/perf.js"></script>
</body>

</html>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

site\public\perf.js

(function (ready) {
    if (document.readyState === "complete" || document.readyState === "interactive") {
        ready();
    } else {
        document.addEventListener("readystatechange", function () {
            if (document.readyState === "complete") {
                ready();
            }
        });
    }
})(function perf() {
    var data = {
        url: window.location.href,
        FP: 0,
        FCP: 0
    };
    new PerformanceObserver(function (entryList) {
        var entries = entryList.getEntries() || [];
        entries.forEach(function (entry) {
            if (entry.name === "first-paint") {
                data.FP = entry.startTime;
                console.log("记录FP: " + data.FP);
            } else if (entry.name === "first-contentful-paint") {
                data.FCP = entry.startTime;
                console.log("记录FCP: " + data.FCP);
            }
        });
    }).observe({ type: "paint", buffered: true });
});
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

# 改进FP和FCP

  • 加快服务器响应速度
    • 升级服务器配置
    • 合理设置缓存
    • 优化数据库索引
  • 加大服务器带宽
  • 服务器开启gzip压缩
  • 开启服务器缓存(redis)
  • 避免重定向操作
  • 使用dns-prefetch进行DNS进行预解析
  • 采用域名分片技术突破同域6个TCP连接限制或者采用HTTP2
  • 使用CDN减少网络跳转
  • 压缩JS和CSS和图片等资源
  • 减少HTTP请求,合并JS和CSS,合理内嵌JS和CSS

site\index.js

const express = require('express');
const logger = require('morgan');
+const compression = require('compression')
const delayConfig = require('./delayConfig');
const app = express();
app.use(logger('dev'));

app.use((req, res, next) => {
    let url = req.url;
    let delay = delayConfig[url];
    if (delay) {
        setTimeout(next, delay);
    } else {
        next();
    }
});
+app.use(compression());
app.use(express.static('public', {
    setHeaders
}));
function setHeaders(res, path) {
    res.setHeader('cache-control', 'no-cache')
}
app.listen(80, () => console.log(`server started at 80`));
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

site\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">
+   <link rel="dns-prefetch" href="//static.360buyimg.com" />
    <title>lighthouse</title>
</head>
<body>
    <img id="banner" style="width:500px;height:300px;" src="/1.png" /><br />
+   <img src="https://pic0.iqiyipic.com/image/20220107/27/cb/v_165289132_m_601_480_270.jpg" />
    <img src="/2.png" />
    <script src="/perf.js"></script>
</body>

</html>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# SI

# 概念

# 如何改进SI

# 最小化主线程工作
# 减少 JavaScript 执行时间
# 确保文本在 webfont 加载期间保持可见
@font-face {
  font-family: 'Pacifico';
  font-style: normal;
  font-weight: 400;
  src: local('Pacifico Regular'), local('Pacifico-Regular'), url(https://fonts.gstatic.com/s/pacifico/v12/FwZY7-Qmy14u9lezJ-6H6MmBp0u-.woff2) format('woff2');
  font-display: swap;
}
1
2
3
4
5
6
7
# web workers

site\public\worker.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">
    <title>webworker</title>
</head>

<body>
    <script>
        function start() {
            //sum();
            const worker = new Worker('/worker.js');
            worker.postMessage(100000000);
            worker.addEventListener('message', function (event) {
                console.log('sum:', event.data);
            });
        }
        function sum() {
            let total = 0;
            for (let i = 0; i < 100000000; i++) {
                total += i;
            }
            console.log('sum:', total);
        }

        start();
    </script>
</body>

</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

site\public\worker.js

self.addEventListener('message', function (event) {
    let total = 0;
    for (let i = 0; i < event.data; i++) {
        total += i;
    }
    self.postMessage(total);
});
1
2
3
4
5
6
7
# 避免强制同步布局和布局抖动

image-20220216203335624

强制同步布局

  • 改变一个元素的特性或者修改其内容有时不仅影响该元素,有时候会导致级联的变化,可能影响该元素的子节点、兄弟节点、父节点的改变,所以每次进行修改时,浏览器都必须重新计算这些改变的影响
  • 如果我们编写的代码不能让浏览器有足够的时间和空间来进行优化,强制浏览器执行大量重新计算,就会造成布局抖动
接口对象 属性名
Element clientHeight, clientLeft, clientTop, clientWidth, focus, getBoundingClientRect, getClientRects, innerText, offsetHeight, offsetLeft, offsetParent, offsetTop, offsetTop, offsetWidth, outerText, scrollByLines, scrollByPages, scrollLeft, scrollHeight, scrollIntoView, scrollIntoViewIfNeeded, scrollTop, scrollWidth
MouseEvent layerX layerY offsetX offsetY
Window getComputedStyle scrollBy scrollTo scroll scrollY
Frame,Document,Image height width
<!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">
    <title>layout</title>
</head>

<body>
    <div id="root"></div>
    <script>
        function reflow() {
            let container = document.getElementById('root');
            let div1 = document.createElement('div');
            div1.innerHTML = 'hello';
            container.appendChild(div1);
            console.log(container.offsetHeight);
            let div2 = document.createElement('div');
            div2.innerHTML = 'world';
            container.appendChild(div2);
            requestAnimationFrame(reflow);
        }
        requestAnimationFrame(reflow);
    </script>
</body>

</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
  • 每次修改DOM,浏览器必须在读取任何布局信息之前先重新计算布局,对性能的损耗十分巨大
  • 避免布局抖动的一种方法就是使用不会导致浏览器重排的方式编写代码 比如批量的读取和写入等
<!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">
    <title>layout</title>
    <style>
        .box {
            width: 100px;
            border: 1px solid green;
        }
    </style>
</head>

<body>
    <div class="box">box1</div>
    <div class="box">box2</div>
    <div class="box">box3</div>
    <div class="box">box4</div>
    <div class="box">box5</div>
    <script src="https://cdn.bootcdn.net/ajax/libs/fastdom/1.0.10/fastdom.js"></script>
    <script>
        let boxes = document.querySelectorAll('.box');
        for (let i = 0; i < boxes.length; i++) {
            const box = boxes[i];
            fastdom.measure(() => {
                const offsetWidth = box.offsetWidth;
                fastdom.mutate(() => {
                    box.style.width = offsetWidth + 5 + 'px';
                });
            });
        }
    </script>
</body>
</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

# LCP

image-20220216203448205

# 记录LCP

site\public\perf.js

(function (ready) {
    if (document.readyState === "complete" || document.readyState === "interactive") {
        ready();
    } else {
        document.addEventListener("readystatechange", function () {
            if (document.readyState === "complete") {
                ready();
            }
        });
    }
})(function perf() {
    var data = {
        url: window.location.href,
        FP: 0,
        FCP: 0,
+       LCP: 0
    };
    new PerformanceObserver(function (entryList) {
        var entries = entryList.getEntries() || [];
        entries.forEach(function (entry) {
            if (entry.name === "first-paint") {
                console.log("记录FP: " + (data.FP = entry.startTime));
            } else if (entry.name === "first-contentful-paint") {
                console.log("记录FCP: " + (data.FCP = entry.startTime));
            }
        });
    }).observe({ type: "paint", buffered: true });

+   new PerformanceObserver(function (entryList) {
+       var entries = entryList.getEntries() || [];
+       entries.forEach(function (entry) {
+           if (entry.startTime > data.LCP) {
+               console.log("记录LCP: " + (data.LCP = entry.startTime));
+           }
+       });
+   }).observe({ type: "largest-contentful-paint", buffered: true });
});
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

# 改进LCP

<link rel="preload" as="style" href="css/style.css">
1

# TTI

  • Time to Interactive(可交互时间) (opens new window)指标测量页面从开始加载到主要子资源完成渲染,并能够快速、可靠地响应用户输入所需的时间
  • webpagetest (opens new window)
  • 虽然 TTI 可以在实际情况下进行测量,但我们不建议这样做,因为用户交互会影响您网页的 TTI,从而导致您的报告中出现大量差异。如需了解页面在实际情况中的交互性,您应该测量First Input Delay 首次输入延迟 (FID)

# 改进TTI

# TBT

  • total Blocking Time(总阻塞时间) (opens new window)指标测量First Contentful Paint 首次内容绘制 (FCP)与Time to Interactive 可交互时间 (TTI)之间的总时间,这期间,主线程被阻塞的时间过长,无法作出输入响应
  • 虽然 TBT 可以在实际情况下进行测量,但我们不建议这样做,因为用户交互会影响您网页的 TBT,从而导致您的报告中出现大量差异。如需了解页面在实际情况中的交互性,您应该测量First Input Delay 首次输入延迟 (FID)

# 如何改进TBT

# FID

  • 首次输入延迟 (FID) (opens new window)是测量加载响应度的一个以用户为中心的重要指标
  • 因为该项指标将用户尝试与无响应页面进行交互时的体验进行了量化,低 FID 有助于让用户确信页面是有效的
  • 首次输入延迟 (FID) 指标有助于衡量您的用户对网站交互性和响应度的第一印象
  • FID 测量从用户第一次与页面交互(例如当他们单击链接、点按按钮或使用由 JavaScript 驱动的自定义控件)直到浏览器对交互作出响应,并实际能够开始处理事件处理程序所经过的时间

# 测试FID

site\public\perf.js

(function (ready) {
    if (document.readyState === "complete" || document.readyState === "interactive") {
        ready();
    } else {
        document.addEventListener("readystatechange", function () {
            if (document.readyState === "complete") {
                ready();
            }
        });
    }
})(function perf() {
    var data = {
        url: window.location.href,
        FP: 0,
        FCP: 0,
        LCP: 0,
+       FID: 0
    };
    new PerformanceObserver(function (entryList) {
        var entries = entryList.getEntries() || [];
        entries.forEach(function (entry) {
            if (entry.name === "first-paint") {
                console.log("记录FP: " + (data.FP = entry.startTime));
            } else if (entry.name === "first-contentful-paint") {
                console.log("记录FCP: " + (data.FCP = entry.startTime));
            }
        });
    }).observe({ type: "paint", buffered: true });

    new PerformanceObserver(function (entryList) {
        var entries = entryList.getEntries() || [];
        entries.forEach(function (entry) {
            if (entry.startTime > data.LCP) {
                console.log("记录LCP: " + (data.LCP = entry.startTime));
            }
        });
    }).observe({ type: "largest-contentful-paint", buffered: true });

+   new PerformanceObserver((entryList) => {
+       for (const entry of entryList.getEntries()) {
+           const FID = entry.processingStart - entry.startTime;
+           console.log('FID:', FID, entry);
+       }
+   }).observe({ type: 'first-input', buffered: true });
});
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

# 改进FID

# CLS

  • [https://web.dev/cls/](http://www.zhufengpeixun.com/strong/html/Cumulative Layout Shift(累积布局偏移))是测量视觉稳定性的一个以用户为中心的重要指标
  • CLS 测量整个页面生命周期内发生的所有意外布局偏移中最大一连串的布局偏移分数

# 如何计算CLS

site\public\perf.js

(function (ready) {
    if (document.readyState === "complete" || document.readyState === "interactive") {
        ready();
    } else {
        document.addEventListener("readystatechange", function () {
            if (document.readyState === "complete") {
                ready();
            }
        });
    }
})(function perf() {
    var data = {
        url: window.location.href,
        FP: 0,
        FCP: 0,
        LCP: 0,
        FID: 0,
        CLS: 0
    };
    new PerformanceObserver(function (entryList) {
        var entries = entryList.getEntries() || [];
        entries.forEach(function (entry) {
            if (entry.name === "first-paint") {
                console.log("记录FP: " + (data.FP = entry.startTime));
            } else if (entry.name === "first-contentful-paint") {
                console.log("记录FCP: " + (data.FCP = entry.startTime));
            }
        });
    }).observe({ type: "paint", buffered: true });

    new PerformanceObserver(function (entryList) {
        var entries = entryList.getEntries() || [];
        entries.forEach(function (entry) {
            if (entry.startTime > data.LCP) {
                console.log("记录LCP: " + (data.LCP = entry.startTime));
            }
        });
    }).observe({ type: "largest-contentful-paint", buffered: true });

    new PerformanceObserver((entryList) => {
        for (const entry of entryList.getEntries()) {
            const FID = entry.processingStart - entry.startTime;
            console.log('FID:', FID, entry);
        }
    }).observe({ type: 'first-input', buffered: true });

+   new PerformanceObserver((entryList) => {
+       var entries = entryList.getEntries() || [];
+       entries.forEach(function (entry) {
+           console.log('entry', entry);
+           if (!entry.hadRecentInput) {
+               data.CLS += entry.value;
+               console.log("CLS: " + data.CLS);
+           }
+       });
+   }).observe({ type: 'layout-shift', buffered: true });
});
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

# 如何改进CLS

  • 始终在您的图像和视频元素上包含尺寸属性
  • 首选转换动画,而不是触发布局偏移的属性动画
  • 除非是对用户交互做出响应,否则切勿在现有内容的上方插入内容

site\public\perf.js

<!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">
    <title>lighthouse</title>
+   <style>
+       @keyframes grow {
+           from {
+               /**transform: scaleY(0);**/
+               height: 200px;
+           }
+           to {
+               /**transform: scaleY(2);**/
+               height: 500px;
+           }
+       }
+       #banner {
+           animation: grow 3s infinite;
+       }
+   </style>
</head>

<body>
+   <img id="banner" style="width:500px;height:300px;" src="/1.png" /><br />
+   <img src="/2.png" />
    <script src="/perf.js"></script>
</body>
</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

# performance面板

# 面板说明

mian_ban_shuo_ming_1641712214460

performance_1641712250550

# 核心流程

# 导航阶段

事件 含义
beforeunload 事件触发于 window、document 和它们的资源即将卸载时
navigationstart 相同的浏览环境下卸载前一个文档结束之时
pagehide 当浏览器在显示与会话历史记录不同的页面的过程中隐藏当前页面时, pagehide(页面隐藏)事件会被发送到一个Window
visibilitychange 当浏览器的某个标签页切换到后台,或从后台切换到前台时就会触发该消息
unload 当文档或一个子资源正在被卸载时, 触发 unload事件
unloadEventEnd 事件处理程序结束之时
send request 发送请求
receive data 接收响应
commitNavigationEnd 提交本次导航结束
domLoading 解析器开始工作时

# 解析HTML阶段

事件 含义
receive data 接收数据
complete loading 完成加载
parseHTML 解析HTML
recalculateStyle 重新计算样式
layout 布局
update layer tree 更新图层树
paint 绘制
raster GPU光栅化
compositor 复合图层
display 显示
dominteractive 主文档的解析器结束工作时
readystatechange interactive(可交互)
domContentLoadedEventStart 所有的需要被运行的脚本已经被解析之时
DOMContentLoaded 当初始的 HTML 文档被完全加载和解析完成之后,DOMContentLoaded 事件被触发,而无需等待样式表、图像和子框架的完全加载
domContentLoadedEventEnd 这个时刻为所有需要尽早执行的脚本不管是否按顺序,都已经执行完毕
domComplete 主文档的解析器结束工作
readystatechange complete(完成)
loadEventStart load事件被现在的文档触发之时
load 当整个页面及所有依赖资源如样式表和图片都已完成加载时,将触发load事件
loadEventEnd load事件处理程序被终止
pageshow 当一条会话历史记录被执行的时候将会触发页面显示(pageshow)事件

main.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">
    <html>
    <head>
        <title>hello</title>
        <style>
            #posts {
                width: 300px;
                height: 300px;
                background-color: green;
            }

            .post {
                width: 300px;
                height: 100px;
                background-color: red;
            }
        </style>
    </head>
<body>
    <div id="posts"></div>
    <script>
        function addPost() {
            const posts = document.getElementById('posts');
            const element = document.createElement('div');
            element.className = 'post';
            element.innerHTML = 'post';
            posts.appendChild(element);
        }
        addPost()   
    </script>
</body>
</html>
</head>
<body>
</body>
</html>
    document.addEventListener('readystatechange', (event) => {
        console.log(event, document.readyState);
    });
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

# Lighthouse优化

# 减少未使用的 JavaScript

# 采用新一代格式提供图片

# 适当调整图片大小

# 推迟加载屏幕外图片

# 移除阻塞渲染的资源

# 减少未使用的 CSS

# 使用视频格式提供动画内容

# 预先连接到必要的来源

# 应避免向新型浏览器提供旧版JavaScript

  • Deploying ES2015+ Code in Production Today (opens new window)
  • Polyfill 和 transform 让旧版浏览器能够使用新的 JavaScript 功能。不过,其中的很多函数对新型浏览器而言并非必需。对于打包的 JavaScript,请采用现代脚本部署策略,以便利用 module/nomodule 功能检测机制来减少传送到新型浏览器的代码量,同时保留对旧版浏览器的支持

# 确保文本在网页字体加载期间保持可见状态

# 未使用被动式监听器来提高滚动性能

# 图片元素没有明确的width和height

  • 请为图片元素设置明确的宽度值和高度值,以减少布局偏移并改善 CLS

# 注册“unload”事件监听器

# 最大限度地减少主线程工作

# 采用高效的缓存策略提供静态资源

# 缩短 JavaScript 执行用时

# 避免链接关键请求

# 请保持较低的请求数量和较小的传输大小

# 最大内容渲染时间元素

# 请避免出现大幅度的布局偏移

  • 这些 DOM 元素对该网页的 CLS 影响最大

# 应避免出现长时间运行的主线程任务

# 避免使用未合成的动画

# 缩减 CSS

# 缩减 JavaScript

# 对图片进行高效编码

# 启用文本压缩

# 初始服务器响应用时较短

# 避免多次网页重定向

# 预加载关键请求

# 使用 HTTP/2

# 请移除 JavaScript 软件包中的重复模块

  • 从软件包中移除重复的大型 JavaScript 模块可减少网络传输时不必要的流量消耗

# 预加载 LCP 元素所用图片

# 避免网络负载过大

# 避免 DOM 规模过大

# User Timing 标记和测量结果

# 尽量减少第三方使用

# 使用 Facade 延迟加载第三方资源

# Largest Contentful Paint 所对应的图片未被延迟加载

# 请勿使用 document.write()

# 具有包含 width 或 initial-scale 的 标记

# 参考

上次更新: 2022/04/15, 05:41:26
×