最近在脉脉上看到一则“性能优化”相关的帖子,不禁想起了在“老东家”:蓝湖一款在线设计协作工具,欢迎大家体验使用哦^-^)进行性能优化专项的日子。一提起性能,大家第一时间能想到的可能是Jmeter、Loadrunner、后端接口性能、响应时间、吞吐量等,但是在真实用户角度,性能的直观体现无关乎页面卡不卡反应快不快2个维度,而这2个维度的体验感受不仅仅关乎于后端性能(接口和数据库等)的提升,也更关乎于前端性能的提升,那今天就借着前端性能这个话题和大家聊一聊前端性能是什么?该怎么做?也将当时性能优化专项时所学、所做、所悟与大家做个分享。

普通用户如何评价一个网站的体验好不好呢?

除了满足他的功能需求以外,用得爽不爽可能是最大的评估因素。这个爽不爽可以简单理解为快不快,好不好看,是不是符合他的操作习惯等等。而这里的快不快就是我们说的性能。

有数据表明,性能在一定程度上跟公司的收益直接相关。
如下所示:

性能 收益
Google 延迟 400ms 搜索量下降 0.59%
Bing 延迟 2s 收入下降 4.3%
Yahoo 延迟 400ms 流量下降 5-9%
Mozilla 页面打开减少 2.2s 下载量提升 15.4%
Netflix 开启 Gzip 性能提升 13.25% 带宽减少50%

为什么性能会跟收益有关系? 因为慢了用户就不用了,用户少了收益自然就下降了。所以提升性能势在必行,而影响性能的因素有很多,大致可分为前端性能和后端性能。诸如前面所讲,本文只讨论前端性能,也就是从用户输入网址或点开链接到页面打开的速度。

应该怎么测试前端的性能呢?


⛺一、先了解网页加载的过程

在讲怎么对前端进行性能测试之前,我们需要先了解从输入网址开始到页面显示出来这个过程中都经过了什么。
大致的流程是:

a.输入网址:在浏览器中输入 URL 或点击链接(一般都是域名);
b.解析域名:浏览器连接到DNS(域名解析系统)将域名解析为对应的 IP 地址;
c.建立连接:然后与这个 IP 地址确定的那台服务器建立起 TCP 网络连接(三次握手);
d.发送请求:随后向服务端发送 HTTP 请求;
e.接收响应:服务端处理完请求后,将数据放在 HTTP 响应里返回给客户端;
f.关闭连接:数据发送完毕之后关闭 TCP 连接(四次挥手);
g.渲染页面:浏览器解析模型(HTML&CSS)、渲染页面(布局、绘制);
h.页面显示:渲染完毕后,页面呈现给用户,并时刻等待响应用户的操作。

如下图所示:

图1
如果从输入网址后,用户可感知的过程出发,大致可以分为:

a.显示白屏:从屏幕空白到第一个画面出来。
b.首次有内容显示:页面在首次有内容显示出来。
c.显示部分内容:页面中显示部分内容。
d.页面可以操作:页面所有内容都显示出来,并且可以正常操作。

如下图所示:
图2

🧸 二、再确定关注哪些性能指标

了解了页面的加载过程,我们就需要针对每个过程制定指标了,这样才能衡量性能好不好。
基于用户可感知的过程(用户体验)的常见指标有:

a.白屏时间(FirstPaint,FP):从屏幕空白到第一个画面出来的时间;
b.首次内容渲染时间(FirstContentful Paint,FCP):渲染出首个文本或图片的时间;
c.最大内容绘制时间(Largest Contentful Paint,LCP):渲染出最大文本或图片的时间;
d.可交互时间(Time To Interactive,TTI):网页需要多长时间才能提供完整交互功能。

怎么衡量这些指标到底好不好呢?

  • 参考业界的标准:以业界标准的指标作为参考,跟业界对齐;
  • 参考竞品的数据:以竞品的性能指标作为参考,向对手学习;
  • 参考监控的数据:采集用户的前端性能数据作为参考;

比如参考业界的指标:

指标
First Contentful Paint 0~1.8s 1.8~3s >3s
First Meaningful Paint 0~2s 2~4s >4s
Speed Index 0~3.4s 3.4~5.8s >5.8s
First CPU Idle 0~4.7s 4.8~6.5s >6.5s
Time to Interactive 0~3.8s 3.9~7.3s >7.3s
Max Potential First Input Delay 0~130ms 130~250ms >250ms
Total Blocking Time 0~200ms 200~600ms 600ms
Largest Contentful Paint 0~2.5s 2.5~4s 4s

参考来自:https://mp.weixin.qq.com/s/-AGuUT0sPKn4pkZuP8gqLA

当然也要关注影响性能的因素,比如页面请求数、图片大小、文件是否压缩、是否使用 CDN 等。
由此可以制定出自己产品的性能指标,比如:

指标类型 指标项 建议/参考值 指标项说明
耗时 finish(s) <5s 页面上所有 HTTP 请求发送到响应完成的时间
耗时 DOMContentLoaded(s) <1s DOM 树构建完成
耗时 Load(s) <3s 页面加载完毕
耗时 First Contentful Paint <1.8s 测量页面从开始加载到页面内容的任何部分在屏幕上完成渲染的时间
耗时 Time to Interactive <3.8s 测量页面从开始加载到主要子资源完成渲染,并能够快速、可靠地响应用户输入所需的时间
耗时 Speed Index(网页速度) <3.4s 衡量页面加载过程中内容从视觉上呈现的速度
耗时 Total Blocking Time(总阻塞时间) <200ms 一个页面的总阻塞时间是在 FCP 和 TTI 之间发生的每个长任务的阻塞时间总和
耗时 Largest Contentful Paint <2.5s 根据页面首次开始加载的时间点来报告可视区域内可见的最大图像或文本块完成渲染的相对时间
耗时 Cumulative Layout Shift <0.1s 测量整个页面生命周期内发生的所有意外布局偏移中最大一连串的布局偏移分数
页面 页面请求数 <80个 页面所有的 HTTP 请求个数
页面 JS / 控制体积和数量,体积越大加载耗时越久,数量越多网络交互耗时越久,可参照同行竞品数据
页面 CSS / 尽可能的少,可参照同行竞品数据
页面 图片 <100k /
页面 页面资源 / 控制资源大小,资源越大加载耗时越久,可参照同行竞品数据
页面 CDN 静态资源使用CDN /

🪭三、选择合适的测试工具

关注的指标定好了,下面看怎么拿到这些指标。

1. Lighthouse:前端性能自动化测试分析工具

Lighthouse是谷歌开源的一款 Web 前端性能自动化测试分析工具,主要用于改进网络应用的质量,我们只需要为其提供一个被测网址,它就能自动进行一些测试,最终会生成一个包括页面性能、PWA(Progressive Web apps,渐进式 Web 应用)、可访问性(无障碍)、最佳实践、SEO 的报告清单提供参考。
我们可以通过 Chrome 开发者工具、Chrome 扩展、命令行、Node Module 使用 Lighthouse。
以 Chrome 开发者工具为例,使用方法如下:

  • TEP 1. 用 Chrome/Edge 打开被测网址,按 F12 ,开启开发者工具,就能看到 Lighthouse 了
    如下所示:

图3

  • STEP 2. 然后点击【Analyze page load】开始分析
    报告如下:

图4
除了上图的评分和指标外,报告中还包括一些提高网页加载速度的优化建议和应用性能的诊断结果。

2.Pingdom Website Speed Test:全面分析影响页面加载速度的原因

Pingdom Website Speed Test可以全面分析影响页面加载速度的原因,分析出页面中每个元素的大小、详细信息以及各个资源的请求时间。这个网站的口号非常好:「互联网很脆弱,成为第一个知道你的网站处于危险之中的人。
网站地址https://tools.pingdom.com/
使用特别简单,只要输入 URL,选择测试的地区,点 「START TEST」 即可,缺点是只能测试不需要登录的页面。
测试报告如下所示:

图5
图6

3.PageSpeed Insights:测试页面在移动和桌面设备上的性能

PageSpeed Insights (PSI) 是由谷歌开发的网站测速工具,主要是用来测试页面在移动设备和桌面设备上的性能,并提供改进页面的建议。
网站地址https://pagespeed.web.dev/
测试报告如下:

图7

4.Chrome DevTools - Performance insights:获取网站性能的可行性深度分析

图8
用法参考:https://juejin.cn/post/7107537105664327716

5.自研的性能测试平台

各公司业务特性、关注重点、性能要求等都不尽相同,因此定制开发前端性能测试平台也很常见,一般会基于 Lighthouse 开发。
比如:
图9
图10

6.前端组件的性能测试

前面说的都是页面的性能测试方法,但其实页面上还包含了大量的组件。比如:输入框、滚动条、日历等。而组件也会存在性能问题,应该如何测试组件的性能呢?
用 Chrome DevTools 就可以。
测试方法:

  • STEP 1. 在 Chrome 中输入 URL ,按 F12,打开 Chrome DevTools,点击「性能」标签
    图11
  • STEP 2. 点左上角的录制按钮,开始录制
    图12
  • STEP 3. 对要测试的组件操作后,停止录制
    图13
  • STEP 4. 停止录制后会自动生成报告,对加载、执行脚本、渲染、绘制的耗时求和即可。这个时间就反映组件的性能
    图14
    当前对组件的耗时没有明确的标准,以使用流畅、无卡顿感为主,如果有性能优化,可用该方法提供性能优化数据。

📙四、制定合适的测试策略

1.重点关注

a.用户访问量高的页面;
b.用户点击量高的功能;
c.网站一级/部分二级重要的页面;
d.监控发现耗时久的页面/功能;
e.用户反馈卡顿的页面/功能。

3.需要注意

a.配置一致:选用与客户配置一致或相似的设备(操作系统、系统版本、系统设置、浏览器、浏览器版本等)进行测试;
b.数据一致:每次测试要用相同的测试数据;
c.操作一致:每次的操作场景要保持一致;
d.网络一致:保持网络稳定,每次测试的网络(WIFI、3G、4G、5G)和网速保持一致;

3.自动测试
搭建固定的前端性能测试环境是有必要的,这样可以保证配置、设备、数据、操作、网络都是一致的,便于性能测试的比对,而且还可以通过自动化的手段重复执行。
比如:

图15

🪒五、自动化示例(以蓝湖性能优化专项为例)

1.指标获取方式

  • FPS:使用前端js脚本获取,在执行操作之前开始获取,执行操作之后结束获取

    js_data/fps-check-start.js
    js_data/fps-check-end.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
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    194
    195
    196
    197
    198
    199
    200
    201
    202
    203
    204
    205
    206
    207
    208
    209
    210
    211
    212
    213
    214
    215
    216
    // ==UserScript==
    // @name fps检测
    // @namespace http://tampermonkey.net/
    // @version 0.1
    // @description try to take over the world!
    // @author VIFEREO
    // @match ://*/*
    // @grant none
    // ==/UserScript==

    (function() {
    class FPS {
    frames = 0;//当前frame

    NORMAL_FPS_THRESHOLDS = 55;// 无操作分界限
    FPS_COUNT = 3;// 连续几个则开始/结束
    SAMPLING = 100;// 每多少毫秒取一次帧率
    KEYCODE = 81;// 快捷键q
    BREAK_OFF_BOTH_ENDS = false;// frameList是否掐头去尾

    lowFpsCount = 0;
    normalFpsCount = 0;
    isStart = false;
    isStartCollect = false;
    isChecked = false;
    prevTime = 0;
    timepiece;
    timestemp = undefined;
    frameList = [];
    info = {}

    dom;
    button;
    span;
    div;
    constructor() {
    this.frameHandler = this.frameHandler.bind(this)
    this.prevTime = Date.now();
    this.dom = document.createElement('div');
    this.dom.style.position = 'absolute';
    this.dom.style.top = '0px';
    this.dom.style.left = '500px';
    this.dom.style.padding = '10px 20px';
    // this.dom.style.width = '200px'
    // this.dom.style.height = '100px'
    this.dom.style.color = '#fff';
    this.dom.style.background = '#A44D4D';
    this.dom.style.zIndex = '9999999';
    this.button = document.createElement('button');
    this.button.innerHTML = '开始'
    const _this = this
    this.button.addEventListener('click', this.clickEvent.bind(_this))
    window.addEventListener("keydown", function (event) {
    if (event.keyCode === _this.KEYCODE) {
    _this.clickEvent.call(_this)
    }
    }, true);
    this.span = document.createElement('div');
    this.div = document.createElement('div');

    document.body.appendChild(this.dom);
    this.dom.appendChild(this.span);
    this.dom.appendChild(this.button);
    this.dom.appendChild(this.div);
    this.frameHandler();
    }
    frameHandler() {

    this.frames++;
    const time = Date.now();
    if (time >= this.prevTime + this.SAMPLING) {
    let fps = Math.round((this.frames * 1000) / (time - this.prevTime))
    if (fps > 60) {
    fps = 60
    }

    this.span.innerHTML = fps;
    this.prevTime = time;
    this.frames = 0;
    if (this.isStart) {
    if (this.BREAK_OFF_BOTH_ENDS) {
    if (!this.isStartCollect) {
    if (fps < this.NORMAL_FPS_THRESHOLDS) {
    this.lowFpsCount += 1;
    } else {
    this.lowFpsCount = 0;
    }
    if (this.lowFpsCount >= this.FPS_COUNT) {
    this.isStartCollect = true
    this.timestemp = new Date();
    }
    } else {
    this.frameList.push(fps)
    if (fps > this.NORMAL_FPS_THRESHOLDS) {
    this.normalFpsCount += 1;
    } else {
    this.normalFpsCount = 0
    }
    if (this.normalFpsCount >= this.FPS_COUNT) {
    this.isChecked = false;
    this.isStartCollect = false;
    this.normalFpsCount = 0;
    this.lowFpsCount = 0;
    // 结束
    this.clickEvent.call(this)
    }
    }
    } else {
    this.frameList.push(fps)
    }
    }
    }
    requestAnimationFrame(this.frameHandler)
    }
    clickEvent() {
    if (!this.isStart) {
    this.isStart = true;
    if (!this.BREAK_OFF_BOTH_ENDS) {
    this.timestemp = new Date();
    }
    this.button.innerHTML = '结束'
    } else {
    this.isStart = false;
    this.timepiece = new Date() - this.timestemp;
    this.timestemp = undefined;
    this.button.innerHTML = '开始'
    if (this.BREAK_OFF_BOTH_ENDS) {
    if (this.frameList.length < 3) return
    this.frameList.splice(this.frameList.length - 3, this.frameList.length - 1);
    }
    this.frameList.sort((a, b) => b - a)
    const p95 = this.frameList[Math.floor(this.frameList.length * 0.95)]
    const avg = (this.frameList.reduce(function (prev, curr, idx, arr) {
    return prev + curr;
    }) / this.frameList.length).toFixed(2)
    const worst = this.frameList[this.frameList.length - 1]
    const vari = this.variance(this.frameList);
    const proportion = this.proportion(this.frameList)
    const str = `耗时:${this.timepiece};\nP95:${p95};\n平均帧率:${avg};\n最差帧率:${worst};\n方差:${vari};\n占比情况:${proportion}`;
    this.info['耗时'] = this.timepiece
    this.info['P95'] = p95
    this.info['平均帧率'] = avg
    this.info['最差帧率'] = worst
    this.info['方差'] = vari
    this.info['fps数据'] = this.frameList


    // console.debug(str);
    // console.debug('frameList end: ', this.frameList);
    // console.debug('info',this.info)
    console.debug(str);
    console.log('frameList end: ', this.frameList);
    console.log('info',this.info)
    // this.div.innerHTML = str
    this.frameList = []
    this.button.innerHTML = '开始'
    }
    }
    variance(arr) {
    // 计算方差
    const total = arr.reduce(function (prev, curr, idx, arr) {
    return prev + curr;
    });
    const avg = total / arr.length
    const squareAdd = arr.reduce(function (prev, curr, idx, arr) {
    const square = Math.pow(curr - avg, 2)
    return prev + square;
    });
    return (squareAdd / arr.length).toFixed(2)
    }

    proportion(arr) {
    // 计算占比
    const count = arr.length;

    let under_20 = 0;
    let twenty_to_thirty = 0;
    let thirty_to_forty = 0;
    let forty_to_fifty = 0;
    let more_then_fifty = 0;

    arr.map(item => {
    if (item <= 20)
    under_20 += 1;
    else if (item <= 30)
    twenty_to_thirty += 1;
    else if (item <= 40)
    thirty_to_forty += 1;
    else if (item <= 50)
    forty_to_fifty += 1;
    else
    more_then_fifty += 1;
    })
    const res = ` \n帧率在20以下的共计:${under_20}个,占比:${(under_20 / count * 100).toFixed(2)}%;\n20-30的共计:${twenty_to_thirty}个,占比:${(twenty_to_thirty / count * 100).toFixed(2)}%;\n30-40的共计:${thirty_to_forty}个,占比:${(thirty_to_forty / count * 100).toFixed(2)}%;\n40-50的共计:${forty_to_fifty}个,占比:${(forty_to_fifty / count * 100).toFixed(2)}%;\n50以上的共计:${more_then_fifty}个,占比:${(more_then_fifty / count * 100).toFixed(2)}%`
    this.info['占比在20以下'] = (under_20 / count * 100).toFixed(2)+'%'
    this.info['占比在20-30'] =(twenty_to_thirty / count * 100).toFixed(2)+'%'
    this.info['占比在30-40'] = (thirty_to_forty / count * 100).toFixed(2)+'%'
    this.info['占比在40-50'] =(forty_to_fifty / count * 100).toFixed(2)+'%'
    this.info['占比在50以上'] = (more_then_fifty / count * 100).toFixed(2)+'%'
    return res
    }

    start(){
    this.isStart = false
    this.clickEvent()
    }
    end(){
    this.isStart = true
    this.clickEvent()
    }
    }
    if(!window.fpsTool){
    window.fpsTool = new FPS()
    }
    window.fpsTool.start()
    })();
  • CFCP 和 CTTI:使用lighthouse命令行获取

    https://github.com/GoogleChrome/lighthouse#using-lighthouse-in-chrome-devtools

2.指标测试维度

  • FPS
    FPS
  • CFCP 和 CTTI
    CFCP

3.指标获取脚本

  • 获取缩放场景的FPS:
    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
    # -*- coding: utf-8 -*-
    """
    @Time : 2022/6/9
    @Author : VIFEREO
    @File : get_result.py
    @Description :
    """

    def test_zoom(driver):
    print(pg.size())
    pg.moveTo(Xmove, Ymove) # 先将光标固定在大概中间位置,再进行缩放,放大缩小算缩放一次
    sleep(2)

    '''---------------开始收集fps,调用js,执行脚本---------------'''
    '''开始收集'''
    driver.execute_script(js.start_js())
    for i in range(1, 11):
    pg.keyDown('command')
    for i in range(1, 20):
    pg.scroll(1)
    sleep(2)
    for i in range(1, 15):
    pg.scroll(-1)
    sleep(2)
    pg.keyUp('command')
    '''结束收集'''
    driver.execute_script(js.end_js())

    sleep(3)
  • 获取平移场景的FPS:
    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
    # -*- coding: utf-8 -*-
    """
    @Time : 2022/6/9
    @Author : VIFEREO
    @File : get_result.py
    @Description :
    """

    def test_translation(driver):
    print(pg.size())
    pg.moveTo(Xmove, Ymove)
    sleep(2)

    '''---------------开始收集fps,调用js,执行脚本---------------'''
    '''开始收集'''
    driver.execute_script(js.start_js())
    # 左右平移
    pg.moveTo(50, Ymove) # 先将光标固定在大概屏幕最左侧位置,再进行平移,这样移动范围大
    for i in range(1, 11):
    pg.dragRel((width - 100), 0, button='middle', duration=1) # 平移距离(width-100)可以在屏幕范围内最大程度的移动
    sleep(0.5)
    pg.dragRel(-(width - 100), 0, button='middle', duration=1)
    sleep(0.5)

    # 上下平移
    pg.moveTo(50, 200) # 先将光标固定在大概屏幕最顶侧位置,再进行平移,这样移动范围大
    for i in range(1, 11):
    pg.dragRel(0, (height - 300), button='middle', duration=1) # 平移距离(width-100)可以在屏幕范围内最大程度的移动
    sleep(0.5)
    pg.dragRel(0, -(height - 300), button='middle', duration=1)
    sleep(0.5)
    sleep(1)
    '''结束收集'''
    driver.execute_script(js.end_js())

    sleep(3)
  • 获取CFCP 和CTTI:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    # 方式一:通过lighthouse获取
    subprocess.call('lighthouse ' + canvas_url + ' --port=9222 --preset=desktop --only-categorie' shell=True)

    # 方式二:通过js脚本获取
    def get_loading(driver):
    res_all = driver.execute_script('return window.performance.timing')
    res_fpt = driver.execute_script(
    'return window.performance.timing.responseEnd - window.performance.timing.fetchStart')
    res_tti = driver.execute_script(
    'return window.performance.timing.domInteractive - window.performance.timing.fetchStart')
    res_ready = driver.execute_script(
    'return window.performance.timing.domContentLoadedEventEnd - window.performance.timing.fetchStart')
    res_load = driver.execute_script(
    'return window.performance.timing.loadEventStart - window.performance.timing.fetchStart')
    res_firstByte = driver.execute_script(
    'return window.performance.timing.responseStart - window.performance.timing.domainLookupStart')
    result = [json.dumps(res_all), res_fpt, res_tti, res_ready, res_load, res_firstByte]
    data_to_excel(result)
    print(result)
    sleep(5)

4.指标数据处理

将脚本获取到数据存入数据库中,并结合grafana看板将图形展示出来。

  • 写入数据:
    a.tti表:写入原数据,每个数据获取5次
    1
    2
    3
    tti_res = ['方案名称','方案描述','尺寸类型','图片数量','产品名称','时间','标签'] 
    tti_res = [planname,opt_type,size_type,picnum,cpname,get_data_time,tag]
    insert_tti(tti_res)
    b.avg表:计算平均值,取掉最大值和最小值,计算3次平均值
    1
    lable_insert_cfcptti_fpsavg(cls.cp, '标注页获取tti', cls.get_data_time)
    c.avgjson表:生成图表
    1
    lable_insert_cfcptti_json(cls.cp, '标注也获取tti', cls.env, cls.get_data_time)
  • 数据展示:
    图16

🧰六、前端性能优化方向

基于上述指标及测试,当我们测试过程中发现前端性能低下,甚至落后于竞品时,该如何去配合开发优化前端性能呢?结合过去的工作经验,以及和开发的沟通学习,原来前端性能可以从以下几个方便考虑。

1.加载前的预处理

  • 使用「dns-prefetch、preconnect」减少DNS解析,建立TCP连接以及执行TLS握手时间,dns-prefetch: 告知浏览器对指定域名进行DNS解析。当后续请求该域名资源时可省掉DNS解析的时间。preconnect: 告知浏览器与指定域名的服务器建立连接。当后续请求该域名资源时,可直接使用已建立好的连接,省掉了 DNS+TCP+TLS 的时间。
    1
    2
    <link rel="dns-prefetch" href="https://s1.static.com">
    <link rel="preconnect" href="https://s1.static.com">
  • 使用「preload/prefetch」让浏览器提前加载需要的资源,preload可以指明哪些资源是在页面加载完成后即刻需要的,浏览器在主渲染机制介入前就进行预加载,这一机制使得资源可以更早的得到加载并可用,且更不易阻塞页面的初步渲染,进而提升性能;prefetch其利用浏览器空闲时间来下载或预取用户在不久的将来可能访问的文档。切记不要将 preload 和 prefetch 进行混用,它们适用于不同的场景,如对同一个资源同时使用 preload 和 prefetch 会造成不必要的二次下载。
    1
    2
    3
    <link href="xx.js" rel="prefetch">
    <!--as表示指定资源类型-->
    <link href="xx.js" rel="preload" as="script">

2.加载过程中

  • 尽可能的减小资源的大小

    (1) 业务代码本身尽可能的不要重复,提高组件化的使用,提示代码的复用率,这里不止是JS,CSS样式也是一样
    (2) 压缩静态资源,一般脚手架都默认会处理,自建项目可检查是否有压缩
    (3) html中的DOM层级控制不要太深以及减少不必要的DOM使用,尽可能发挥伪元素及CSS的使用
    (4) 检查项目的依赖包是否有重复引用的情况,不同的依赖包可能引用了同一个不同版本的包,可通过webpack-bundle-analyzer插件分析查看
    (5) UI组件库或其他库使用babel-plugin-import插件进行按需加载
    (6) 组件按需加载,使用AsyncComponent仅加载首屏组件
    (7) 动态导入第三方比较大的模块,import(‘/modules/echart.js) .then((module) => {}),但不要滥用,结合实际场景使用
    (8) 减小第三方库的大小,如Moment.js/lodash等,使用轻量级别替代方案或者自己重新实现
    (9) 对首评秒开要求较高的,可对首屏请求的接口进行拆分,快速响应首屏需要用到的字段,其他的数据异步加载
    (10) 使用tree shaking,当我们在项目中引入其他模块时,他会自动将我们用不到的代码,或者永远不会执行的代码摇掉,在Uglify阶段查出,不打包到bundle中
    (11) HTTP头部Cookie的精简,去除不必要的Cookie,静态资源做独立域名部署,避免请求携带Cookie
    (12) HTTP头部开启gzip压缩,可大大减小网络传输的数据量
    (13) HTTP头部开启keep-alive
    (14) 升级HTTP到2.0,2.0的头部压缩,减少了数据传输量,能够节省消息头占用的网络的流量,且还有多路复用等优势

  • 尽可能的减少资源的次数

    (1) JS/CSS数量不可太分散,避免一下发起太多的请求,必要将部分资源合并在一起,减少请求的数量。但是在合并的过程中需求在体积和数量之间权衡,并不是越少越好,可将最大的体积控制在一个范围内进行合并
    (2) 部分小体量级别的JS/CSS可内联到HTML中,减少请求数量
    (3) 减小预检请求OPTIONS的发起,可通过服务端设置Access-Control-Max-Age字段或改为发起简单请求
    (4) 取消无效请求,表单提交频繁点击,路由切换时还有未完成的请求。这些都会产生无效请求,对服务器和用户体验都是不好的
    (5) 缓存策略

    a.开启http强缓存与协商缓存,对于不同类型的资源使用不同的缓存策略
    b.静态资源开启CDN服务
    c.对于不常变化的数据包括外部JS/CSS资源,可进行前端浏览器缓存,减少请求,但此类缓存需设定好清除及更新的机制

  • 其他资源优化

    (1) 图片webp使用,对于支持的设备使用webp
    (2) 图片裁剪,针对使用场景进行相应的裁剪
    (3) 大图不要打包在项目中,上传到单独的静态资源服务器或是CDN中
    (4) 图片上传前进行压缩,切记不要使用原图
    (5) 设置图片标签尺寸大小,防止图片加载中导致页面布局抖动,影响CLS指标的数值
    (6) 超出屏幕外的图片开启懒加载
    (7) 对于项目中大量的小图标可使用iconfont字体方案
    (8) 使用第三方字体库时尽可能按需文字生成
    (9) 加载字体的时候会导致页面文字有一定的闪烁抖动,可在进入需要用到的页面前使用preload提前进行加载

3.页面渲染时

  • 开启骨架屏,提升用户体验,避免加载到渲染过程中都是白屏阶段
  • 对于大量列表的滚到使用虚拟列表
  • 尽量多使用CSS3动画
  • 使用 requestAnimationFrame 监听帧变化,使得在正确的时间进行渲染
  • 合理使用CSS,避免通配符,最大化样式继承,少用标签选择器,减少过深嵌套等

4.用户界面交互

  • 减少页面重排、重绘
  • 防抖节流的使用
  • 合理使用 requestAnimationFrame 动画代替 setTimeout
  • 开启GPU加速,CSS中可使用以下属性(CSS3 transitions、CSS3 3D - transforms、Opacity、Canvas、webGL、Video)来触发 GPU 渲染
  • 减少 JavaScript 脚本执行时间,把一些和 DOM 操作无关且耗时的任务放到 Web Workers 中去执行
  • 对未来某个时间内需要执行动画的元素,将其标记为 will-change,这样渲染引擎会将该元素单独生成一个图层

🧰七、总结

本文从网页加载的过程、过程中关注的指标、获取指标的工具和测试的策略较为系统的介绍了前端性能测试的思路,希望能够带给大家一些启发。没有所谓的绝对优化,都需要结合当前项目的应用场景及对项目全量的性能分析,找到某个方向的不足,针对性地优化,选择合适的方案。希望大家都能找到自己合适的优化方向,把项目优化的妥妥的啦~~