1. 为什么要关注Web性能 #

2. Lighthouse #

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

3. Lighthouse性能指标 #

3.1 FP和FCP #

3.1.1 记录FP和FCP #

3.1.1.1 安装依赖 #
npm install express morgan compression --save
3.1.1.2 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`));
3.1.1.3 delayConfig.js #

site\delayConfig.js

const delayConfig = {
    "/index.html": 100
}
module.exports = delayConfig;
3.1.1.4 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>
3.1.1.5 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 });
});

3.1.2 改进FP和FCP #

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

3.2 .SI #

3.2.1 如何改进SI #

3.2.1.1 最小化主线程工作 #
3.2.1.2 减少 JavaScript 执行时间 #
3.2.1.3 确保文本在 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;
}
3.2.1.4 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>

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

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

强制同步布局

接口对象 属性名
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>
    <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>

3.3 LCP #

3.3.1 记录LCP #

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

3.3.2 改进LCP #

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

3.4. TTI #

3.4.1 改进TTI #

3.5 TBT #

3.5.1 如何改进TBT #

3.6 FID #

3.6.1 测试FID #

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

3.6.2 改进FID #

3.7 CLS #

3.7.1 如何计算CLS #

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

3.7.2 如何改进CLS #

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

4. performance面板 #

4.1 面板说明 #

4.2 面板说明 #

4.3 核心流程 #

4.3.1 导航阶段 #

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

4.3.2 解析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);
    });

4. Lighthouse优化 #

4.1 减少未使用的 JavaScript #

4.3 适当调整图片大小 #

4.4 推迟加载屏幕外图片 #

4.5 移除阻塞渲染的资源 #

4.6 减少未使用的 CSS #

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

4.8 预先连接到必要的来源 #

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

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

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

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

4.13 注册“unload”事件监听器 #

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

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

4.16 缩短 JavaScript 执行用时 #

4.17 避免链接关键请求 #

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

4.19 最大内容渲染时间元素 #

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

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

4.22 避免使用未合成的动画 #

4.23 缩减 CSS #

4.24 缩减 JavaScript #

4.25 对图片进行高效编码 #

4.26 启用文本压缩 #

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

4.28 避免多次网页重定向 #

4.29 预加载关键请求 #

4.30 使用 HTTP/2 #

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

4.32 预加载 LCP 元素所用图片 #

4.33 避免网络负载过大 #

4.34 避免 DOM 规模过大 #

4.35 User Timing 标记和测量结果 #

4.36 尽量减少第三方使用 #

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

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

4.39 请勿使用 document.write() #

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

5.参考 #