Lighthouse Web性能

关注Web性能的重要性

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

Web 性能是客观的衡量标准,是用户对加载时间和运行时的直观体验。Web 性能指页面加载到可交互和可响应所消耗的时间,以及页面在交互时的流畅度——滚动是否顺滑?按钮能否点击?弹窗能否快速打开,动画是否平滑?Web 性能既包括客观的度量如加载时间,每秒帧数和到页面可交互的时间;也包括用户的对页面内容加载时间的主观感觉。

页面响应时间越长,越多的用户就会放弃该网站。重要的是,通过使体验尽可能早地变得可用和交互,同时异步地加载长尾体验部分,来最大程度地减少加载和响应时间,并添加其他功能以降低延迟。

Lighthouse

  • lighthouse是一个开源的自动化工具,用于改进网络应用的质量
  • Lighthouse报告分析了加载页面生命周期中的各种性能指标
  • webpagetest是一个免费的网站性能测试工具
lighthouse https://m.jd.com --locale zh --quiet --chrome-flags="--headless"  --only-categories=performance  
lighthouse https://m.jd.com --locale zh --quiet --chrome-flags="--headless"  --only-categories=performance  

Lighthouse性能指标

image.png

FP和FCP

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

记录FP和FCP

安装依赖
npm install express morgan compression --save
npm install express morgan compression --save
site\index.js

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`));
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`));
delayConfig.js

site\delayConfig.js

const delayConfig = {
    "/index.html": 100
}
module.exports = delayConfig;
const delayConfig = {
    "/index.html": 100
}
module.exports = delayConfig;
index.html

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>
<!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>
perf.js

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 });
});
(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 });
});

改进FP和FCP

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

site\index.js

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`));
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`));

index.html

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

SI Speed Index

  • Speed Index(速度指数)表明了网页内容的可见填充速度
  • 速度指数衡量页面加载期间内容的视觉显示速度

如何改进SI

最小化主线程工作
减少 JavaScript 执行时间
确保文本在 webfont 加载期间保持可见
  • 确保文本在 webfont 加载期间保持可见
  • 字体通常是需要一段时间才能加载的大文件。一些浏览器在字体加载之前隐藏文本,导致不可见文本 (FOIT) 闪烁。
  • 通过包含font-display: swap在您的@font-face风格中,您可以在大多数现代浏览器中避免 FOIT
@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;
}
@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;
}
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>
<!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>

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);
});

self.addEventListener('message', function (event) {
    let total = 0;
    for (let i = 0; i < event.data; i++) {
        total += i;
    }
    self.postMessage(total);
});

避免强制同步布局和布局抖动

image.png

image.png

强制同步布局

  • 改变一个元素的特性或者修改其内容有时不仅影响该元素,有时候会导致级联的变化,可能影响该元素的子节点、兄弟节点、父节点的改变,所以每次进行修改时,浏览器都必须重新计算这些改变的影响
  • 如果我们编写的代码不能让浏览器有足够的时间和空间来进行优化,强制浏览器执行大量重新计算,就会造成布局抖动
接口对象 属性名
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>
<!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>
  • 每次修改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>
<!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>

LCP (最大内容绘制)

image.png

记录LCP

perf.js

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 });
});
(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 });
});

改进LCP

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

TTI (可交互时间)

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

改进TTI

TBT (总阻塞时间)

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

如何改进TBT

FID (首次输入延迟)

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

测试FID

perf.js

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 });
});
(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 });
});

改进FID

CLS

  • https://web.dev/cls/是测量视觉稳定性的一个以用户为中心的重要指标
  • CLS 测量整个页面生命周期内发生的所有意外布局偏移中最大一连串的布局偏移分数

如何计算CLS

perf.js

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 });
});
(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 });
});

如何改进CLS

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

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

performance面板

面板说明

image.png

4.2 面板说明 #

image.png

核心流程

导航阶段

事件 含义
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>
<!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);
    });
    document.addEventListener('readystatechange', (event) => {
        console.log(event, document.readyState);
    });

Lighthouse优化

减少未使用的 JavaScript

  • Remove unused JavaScript

  • 请减少未使用的 JavaScript,并等到需要使用时再加载脚本,以减少网络活动耗用的字节数

    采用新一代格式提供图片

  • Serve images in modern formats

  • WebP 和 AVIF 等图片格式的压缩效果通常优于 PNG 或 JPEG,因而下载速度更快,消耗的数据流量更少

适当调整图片大小

  • Properly size images
  • 提供大小合适的图片可节省移动数据网络流量并缩短加载用时

推迟加载屏幕外图片

  • Defer offscreen images
  • 建议您在所有关键资源加载完毕后推迟加载屏幕外图片和处于隐藏状态的图片,从而缩短可交互前的耗时

移除阻塞渲染的资源

  • Eliminate render-blocking resources
  • 资源阻止了系统对您网页的首次渲染。建议以内嵌方式提供关键的 JS/CSS,并推迟提供所有非关键的 JS/样式

减少未使用的 CSS

  • Remove unused CSS
  • 请从样式表中减少未使用的规则,并延迟加载首屏内容未用到的 CSS,以减少网络活动耗用的字节数

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

  • Use video formats for animated content
  • 使用大型 GIF 提供动画内容会导致效率低下。建议您改用 MPEG4/WebM 视频(来提供动画)和 PNG/WebP(来提供静态图片)以减少网络活动消耗的字节数

预先连接到必要的来源

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

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

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

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

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

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

注册“unload”事件监听器

  • Legacy lifecycle APIs to avoid
  • unload事件不会可靠地触发,而且监听该事件可能会妨碍系统实施“往返缓存”之类的浏览器优化策略。建议您改用pagehidevisibilitychange事件

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

  • Minimize main thread work
  • 建议您减少为解析、编译和执行 JS 而花费的时间。您可能会发现,提供较小的 JS 负载有助于实现此目标

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

缩短 JavaScript 执行用时

  • Reduce JavaScript execution time
  • 建议您减少为解析、编译和执行 JS 而花费的时间。您可能会发现,提供较小的 JS 负载有助于实现此目标

避免链接关键请求

  • Avoid chaining critical requests
  • 下面的关键请求链显示了以高优先级加载的资源。请考虑缩短链长、缩减资源的下载文件大小,或者推迟下载不必要的资源,从而提高网页加载速度

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

最大内容渲染时间元素

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

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

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

避免使用未合成的动画

缩减 CSS

  • Minify CSS
  • 缩减 CSS 文件大小可缩减网络负载规模

缩减 JavaScrip

  • Minify JavaScript
  • 如果缩减 JavaScript 文件大小,则既能缩减负载规模,又能缩短脚本解析用时

对图片进行高效编码

  • Efficiently encode images
  • 如果图片经过了优化,则加载速度会更快,且消耗的移动数据网络流量会更少

启用文本压缩

  • Enable text compression
  • 对于文本资源,应先压缩(gzip、deflate 或 brotli),然后再提供,以最大限度地减少网络活动消耗的字节总数

初始服务器响应用时较短

避免多次网页重定向

预加载关键请求

  • Preload key requests
  • 建议使用 <link rel=preload> 来优先提取当前在网页加载后期请求的资源

使用 HTTP/2

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

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

预加载 LCP 元素所用图片

避免网络负载过大

避免 DOM 规模过大

  • Avoid an excessive DOM size
  • 大型 DOM 会增加内存使用量、导致样式计算用时延长,并产生高昂的布局重排成本

User Timing 标记和测量结果

尽量减少第三方使用

  • Loading Third-Party JavaScript
  • 第三方代码可能会显著影响加载性能。请限制冗余第三方提供商的数量,并尝试在页面完成主要加载后再加载第三方代码

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

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

请勿使用 document.write()

  • Uses document.write()
  • 对于连接速度较慢的用户,通过 document.write() 动态注入的外部脚本可将网页加载延迟数十秒

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

参考