阅读 2725

兄dei,这次我们来聊聊前端性能优化(起航篇)

导语:最近在处理性能优化相关的工作,发现这块的内容涉及到比较多的知识点和有意思的工具,所以整理一下,便于以后查阅(毕竟迈入了30岁,很多东西不整理就容易忘😭😭),也希望对屏幕前正在看此文的你有一点帮助和启发。

说完废话,我们言归正传,来聊一聊我们本次的话题:《前端性能优化》

本文所有相关的demo展示都传到了github上: github.com/ycvcb123/pe…

本文目录:

为什么要做性能优化?

回答这个问题前,我们来看下一些网站的统计数据

image.png

数据来源: www.pingdom.com/blog/page-l…

随着网页加载时间的变长,用户的跳出率会越来越高。(高就高呗,又不是不能正常使用,i don‘t care~)

我们接着看下面这张图:

image.png

数据来源: www.websitebuilderexpert.com/building-we…
        www.thinkwithgoogle.com/feature/tes…

    从上面的几个例子来看,亚马逊慢一秒就会损失1.6亿美元,沃尔玛每快一秒,就会增加2%的转化率,速卖通将页面加载的时间减少了36%,订单增加了10.5%,新客户转化率增加了27%,等等...

    我们可以得出结论:网站的访问量及用户的持久性其实在一定程度上取决于其性能,如果一个网站响应耗时久,占用大量的cpu等,往往就会导致用户流失,从而给网站带来一些直接的经济损失。(谈钱,你还能不care吗?😃)

聊完了为啥要做性能优化,接下来我们来聊一聊性能优化的历史。

性能优化发展历史?

读史让人明智 -- 某个名人说过

参考一些资料整理了个时间轴(年份可能略有差异,不重要)

image.png

1. 2007年 -- 雅虎军规,检测工具 yslow

雅虎军规, yslow

一、雅虎提出的35条军规:包括我们熟悉的(减小cookie体积,减少http请求数,减少dns查询,避免重定向,避免404页面,等等。。。大家可以通过上面的官方链接研究一下)。

二、yslow: 基于雅虎35条军规中的一部分军规开发的检测工具。(但是现在安装地址点进去已经404了,😓),想了解的同学看下这片文章 网站性能评分工具Yslow 使用教程

可以说,2007从雅虎军规开始,开启了一个前端性能优化时代的新纪元(像极了2007年拍摄的钢铁侠1,开启了整个漫威宇宙),意义巨大。

2. 2008年 -- google推出WebPageTest

WebPageTest

image.png

可以选择测试地点,浏览器类型,在Advanced Settings高级选项中还能设置链接类型,测试次数,等等。。。非常有意思的一个工具,大家可以自己去尝试一下。

3. 2009年 -- google推出SPDY协议

SPDY:其实就是HTTP2的前身,大家可以看下这边文章,介绍的很详细 SPDY简介

4. 2010年 -- FaceBook推出BigPige

BigPige,是针对服务端渲染(SSR)的优化。

SSR: 服务端准备好所有内容,拼接成完整的html文档,返回给前端。

简单来说,就是把页面分成不同的模块,当有模块加载完成,就返回给前端,不用等所有的模块都加载完成。

如下图,对图中的 34 步做了优化:

image.png

image.png

更多关于BigPige的技术细节,大家可以参考 bigPipe 原理分析 ,里面解释的很详细。

5. 2015年 -- HTTP-WG基于googleSPDY协议正式推出http2,google推出RAIL模型PageSpeed insights 页面性能检测工具

一、HTTP2:介绍这个的文章很多,一文读懂 HTTP/2 特性HTTP2 详解 这两篇解释的很好,它的主要优点就是(关于下面的后两点,后面会有具体的demo 说明,大家先继续往下看~):

  • 二进制格式传输代替文本传输
  • 多路复用打破最大链接数限制
  • 服务端推送减少TTFB时间

二、RAIL模型: 一图胜千言,看看下面这图,眼熟不?(¬◡¬)✧

image.png

google开发者手册 对这个模型有一个很详细的解释 Measure performance with the RAIL model

简单做一个说明:

  • Response:轻触后100ms内有反馈。
  • Animation:保证动画每一帧的渲染都在16ms 以内,之前写过一遍相关的文章,大家感兴趣的可以看下,解释了为啥要保证一帧在16ms 以内,兄dei,听说你动画很卡?
  • idle: 合理的利用空闲时间,保证任务尽量在50ms 完成。(这里等下介绍long task的时候,会有详细说明)。
  • load:加载时间,重要的内容尽量在1s 内完成。

三、PageSpeed insights性能检测工具

PageSpeed insights

使用界面: image.png

结果输出页:(是不是像极了lighthouse,个人认为,这个其实就是lighthouse 的前身)

image.png

6. 2016年 -- google推出lighthouse检测工具和PWA方案,还有最佳开发者手册

一、lighthouse:页面性能指标的检测工具,最简单的使用方式,就是打开chrome 控制台使用,其余还支持插件,node,命令行等使用形式。(下面也会有详细的介绍)

二、PWA:渐进式web 应用,官方也有一个很详细的介绍 Progressive Web Apps

它其实就是补足了,现在web 应用的一些不足,比如,消息推送,离线缓存,没法从屏幕上直接打开,等等...

三、谷歌最佳开发者手册web.dev 有你想知道的很多东西(比如等下会说到的各个性能指标的定义,都有着很清楚的解释~),可以慢慢研究一下。

7. 2017年 -- google推出AMP,百度推出MIP

两者其实差不多是一样的,MIP 继承了很多 AMP 的思想,目的都是为了加快移动页面的访问速度,主要原理都是通过自带的runtime 协调资源的加载时机和优先级,保证页面的快速渲染。

具体的介绍看下张鑫旭大神写的 移动页面加速google的AMP和百度的MIP简介,还有谷歌AMP和百度MIP,你选哪个? 这篇也很不错。

8. 2018年 -- HTTP-WG推出HTTP3

HTTP3:这里有一篇关于HTTP3的详解 5 分钟看懂 HTTP3,感兴趣的同学可以了解一下。

8. 2020年 -- google推出web-vitals新一代的性能指标评估方案

image.png

包含了三个重要指标:(在等下的指标介绍中会有详细的解释)

  • LCP:最大内容绘制时间
  • FID:首次用户输入延时
  • CLS:累计布局偏移

说了这么多,以上就是性能发展的一个大概历史,接下来我们来聊一聊具体的性能指标:

性能指标介绍

在介绍前我们简单了解下,主要是谁在负责这个事?

这就要提一下大名鼎鼎的W3C了!

它在2010年成了性能工作组,由谷歌和微软的工程师担任主席,主要就是制定衡量 Web 应用性能的方法和 API

感兴趣的可以看下性能工作组的官网 Web Performance Working Group,对每一个提案都有一个非常详细的说明,如下,是我按照其中给的时间,整理了下大概的一个发展过程,能看到其中有我们很熟悉的

  • 计时方法:Navigation timing / paint timing / User timing.

  • 专门处理上报的:Beacon.

  • 高精度时间:High Resolution Time.

image.png

说句题外话,在整个前端性能优化的历史演进中,不管是工具,标准,新的技术,等等。。。 google都占据了举足轻重的作用,也许就是这样的不断发展慢慢卷没了IE...

image.png

我们现在来看下,性能相关的一些重要指标:

右边这个图用过lighthouse的同学应该都见过,就是一个性能的总评分,而这个分数就是通过右边六项指标加权平均得到的,我们来具体看下这六项指标。

image.png

  • FCP: 首次内容绘制(可以理解为白屏时间) -- 以页面首次加载的时间为起点,来报告可视区内最大元素的绘制的相对时间
  • LCP: 最大内容绘制 -- 根据页面首次开始加载的时间点来报告可视区域内可见的最大图像或文本等的相对时间

还是那句话,一图胜千言:

image.png

  • SI: Speed Index (可见区域内,显示页面可见部分的平均时间),简单理解就是一个填充速度指标

image.png

看上下两张图,上部分在1s的时候就加载了93%左右的内容,下半部分在11s才加载到这么多,虽说,两个加载完成的时间都是12s,但是明显上面要比下面加载要快,转化成图表:

image.png

带入积分公式:

image.png

因为求的是不可见部分的积分,所以这个指标,越小越好:

image.png

  • TTI: 可持续交互时间

如下图: 指的是从最后一个长任务结束后,5秒的时间内,主线程是空闲的。

image.png

在这里接上上文中留下的问题,为什么RAIL模型中建议,处理任务的时间好是50ms, 这里我们来解释下长任务

image.png

RAIL模型中,建议反馈的时间最好在100ms以内,但是在输入处理前可能还有写其他的工作要做,有一个阻塞的时间,所以长任务的时间定为50ms,是为了确保100ms内对输入作出响应,也就解释了为啥 idle中建议任务在50ms内完成,看官方提供的这个图:

image.png

综上,长任务就是超过50ms的任务。

  • TBT: 总阻塞时长

从FCP到TTI所有长任务阻塞时间之和 TBT = (longtaskTime1 - 50ms)+ (longtaskTime12- 50ms)+(longtaskTime3 - 50ms)+ ...

  • CLS: 累计布局偏移(可以理解为视觉稳定性的一个指标)

具体可以看下官方的一个视频例子:storage.googleapis.com/web-dev-ass…

image.png

  • FID: 首次输入延时

从用户第一次与页面交互直到浏览器对交互作出响应,并实际能够开始处理事件处理程序所经过的时间

image.png

解释下上面这个图,从FCPTTI这段时间内,用户输入遇到长任务后等待的时间,就是 FID

ok,综上就是我们需要知道的大部分指标的含义,接下来我们看下要怎么获取这些指标:

Performance TimeLine 闪亮登场!

html中有个window对象,在这个对象上挂载了许多我们熟悉的apiwindow.location,window.document,window.postmessagewindow.localStorage等等,web的性能标准,则是在window上添加了一个performance属性,返回一个Performance对象,包含了很多衡量性能的属性和方法。

  • 提供了两个高精度的时间:

performance.now() : 相对于创建浏览器上下文时间递增,不受系统时间的影响,是一个相对值。 performance.timeOrigin: 性能检测开始的时间

  • 三个方法:

getEntries()getEntriesByType()getEntriesByName()

getEntries() 返回一个对象数组包含 Web 应用程序整个生命周期的各种性能数据,getEntriesByTypegetEntriesByName 就是按照typename 做了一个筛选。

  • 两个对象:

PerformanceEntryPerformanceObserver

上面说的getEntries获取的对象就是继承于PerformanceEntry.

重点说下 PerformanceObserver 这个对象:

image.png

在控制台输入:

const perfObserver = new PerformanceObserver(entryList => {
    console.log(entryList.getEntries());
})

perfObserver.observe({type: 'paint', buffered: true});
复制代码

在这里看就能看到上文说过的,FCP的绘制时间。

image.png

指标数据基本就是通过这个PerformanceObserver对象观察得到的。

多说一句,上面有个属性buffered: true

在我们创建 PerformanceObserver 之前,可能有些 entry 已经发生了,这中间有时间间隔,导致我们观察到的不是正确的。举个例子来说。 假设我们想现在访问这个网页的 entryTypepaintentry ,但是我们打开控制台的时候,网页大概率已经加载完毕了,肯定观察不到 entry了,bufferd就出场了,bufferd确保我们能拿到PerformanceObserver创建之前的 entry

网上有很多基于 PerformanceObserver,获取性能指标的方法,推荐大家去看下 perfume.js 的源码,可以更加熟练的使用 PerformanceObserver

聊完了指标的意义和获取方式,我们来聊一下第四部分性能优化方案

性能优化方案?

要优化,肯定要先找问题,找问题,就要先看指标,我们如何方便快速的查看刚才描述的那些指标呢?

chrome 开发者工具,带给我了我们很大便利!

performance 模块:

Dimensions Moto G4.png

network 模块:

Performance Network.png

上面两个模块可以帮我很好的分析定位问题,但是你光让我看着这些东西,就来判断具体要做那些优化,说实话还有感觉有点不方便的,所以下面介绍下,chrome的一个大杀器 lighthouse:

Performance.png

点击Generate report,来生成报告。

Performance.png

上图不但告诉了我们评分,指标,最最重要的,还告诉了我们该怎么做!!!

下面是一些常见的问题及一些对应的解决办法:

图片相关:

1. 使用有效恰当的图片

image.png

2. 使用下一代图片格式

image.png

3. 图片懒加载

image.png

先看第一个问题,除了要注意使用图片的大小外,还要注意图片的格式,我们看下现有的图片格式大概分哪几类:

  1. PNG:无损压缩、质量高、体积大、支持透明

应用场景:色彩简单,但是对图片要求比较高的图片(网站的logo)

  1. JPG:有损压缩、体积小、加载快、不支持透明

应用场景:背景图片,轮播图,banner

  1. GIF:基于LZW压缩算法的无损压缩

应用场景:短,小,不容易用样式实现的动画

  1. APNG:动态PNG,像GIF格式一样播放动态图片,并且拥有GIF不支持的24位图像和8位透明性

应用场景:同GIF

  1. SVG:文本文件、体积小、不失真、兼容性好

应用场景:地图,股票k线图

  1. WEBP:支持透明,支持有损压缩,无损压缩,可以和GIF一样动

应用场景:能用就用,做好降级处理

  1. BASE64:无需请求,嵌入HTML

应用场景:小图标

看下一个推荐的选择图片的公式:

image.png

不要小看图片格式的选择,图片资源的请求几乎占据了网站资源请求量的一半:

image.png

合理的使用图片尺寸和格式,可能会对加载速度带来很大的优化效果。

我们接着看下面的两个问题:

使用webp 格式的图片和图片懒加载

这两者其实可以合成一个套方案

VUE-LAZYLOAD + WEBP

首先如何获取webp格式的图片?

除了网上的一些生成工具外,在项目中我们可以使用webpack插件imagemin-webp-webpack-plugin 来生成

image.png

会得到一个和原图hash值一样的,后缀为webp格式的图片。

引用方法:

<img v-lazy class="bg" lazysrc="../imgnew/about-2-gzh.png" />
复制代码
!detect.DEV &&
Vue.use(VueLazyload, {
    filter: {
        progressive(listener: any, options: any) {
            // 把lazysrc的值赋给src
            listener.src = listener.el.getAttribute('lazysrc');
        },
        webp(listener: any, options: any) {
            if (isSupportWebp() && !/\.svg$/.test(listener.src)) {
                listener.src += '.webp';
            }
        },
    },
});
复制代码

看下webpack 的具体配置:

//...
plugins: [
isProduction && new ImageminWebpWebpackPlugin({
    config: [
        {
            test: /\.(jpe?g|png)/,
            options: {
                quality: 75,
            },
        },
    ],
    overrideExtension: false, // 这里不会替换后缀名,而是往后加添加 xxxxx.png.webp
    }),
]

// 特别注意,下面这个修改是为了解决,引用时相对路径找不到的问题
isProduction && config.module.rule('vue')
.use('vue-loader')
.tap(options => {
    return {
        ...options,
        transformAssetUrls: { img: ['src', 'lazysrc'] }
    }
})
复制代码

看下效果,滑动到底步,图片懒加载,且在支持webp的情况下显示webp格式的图片:

image.png

js相关问题

image.png

观察上面建议点,大概可以把问题归结为:

加快js文件的下载,解析,执行效率。

我们看以下几个方案:

  1. 开启现代浏览器模式:

我们在用vue, 或者 react构建项目的时候,都会有一步用babel-loaderes6+的语法转成es5的操作,这是为了兼容各个不同的浏览器,但其实现在es6的语法,很多浏览器都是支持的,在支持的情况下,我们不再转换成冗长且执行效率低下的es5语法,可以大大的提升js的执行效率,对于不支持的情况,在用es5做一个兼容就好。

原理是按照browserslist,构建两次,一次生成现代模式,一次生成老的模式,通过判断浏览器是否支持module,来选择新旧模式。

image.png

vue-cli中,已经为我们提供了开启这种现代浏览器模式的方式。

vue-cli-service build --modern
复制代码
  1. 按需加载(以vue项目举例)

如果同一个页面,有着多种不同的模版,建议使用如下的方式加载:

image.png

同一个文件中,如果有大段的逻辑不是需要立刻执行,建议通过下面这种方式执行:

image.png

webpackPrefetch: true 是为了让在浏览器空闲的时候加载这段代码,而不会因为触发后再加载,影响到对用户的反馈。

  1. 合理的分包策略

这里提供一个大概的参考,以vue项目为例:

  1. 为了防止node_modules打包出来的chunk-vendor过大,可以将其中大于300kb的模块单独打包出来。

  2. 为了防止一些公共模块重复打包,可以把公用的模块打包到一起,根据打包后的体积在考虑如何进一步更细致的拆分。

  3. vue, vuex, vue-router,这类的库,变动可能非常小,可以通过cdn引入,构建时候不要在打包。

//...

// 通过cdn的方式引入
config.externals({
    vue: 'vue',
    'vue-router': 'vue-router',
    vuex: 'vuex',
});

const assets = [
    { path: `/libs/vue/2.6.12/vue.esm.min.js`, type: 'js' },
    { path: `/libs/vue-router/3.5.1/vue-router.esm.min.js`, type: 'js' },
    { path: `/libs/vuex/3.1.2/vuex.min.js`, type: 'js' },
];

new HtmlWebpackIncludeAssetsPlugin({
    assets,
    append: true,
}),
//...

// 拆包
config.optimization.splitChunks({
        minSize: 300 * 1024,
        maxInitialRequests: Infinity,
        chunks: 'all',
        cacheGroups: {
            matchVendor: {
                test: /[\\/]node_modules[\\/]/,
                name(module) { // 超过300kb,单独打包
                    const packageName = module.context.match(/[\\/]node_modules[\\/](.*?)([\\/]|$)/)[1];
                    return `chunk&&&&&&${packageName.replace('@', '')}`;
                },
                priority: 10
            },
            common: {
                minSize: 100 * 1024,
                minChunks: 2, // 有两个或者两个以上chunk都使用到了,就单独打包出来
                priority: 0,
                reuseExistingChunk: true,
                name: 'chunk-common',
            }
        }
    })
复制代码

image.png

  1. 迎合v8 引擎做优化

我们先看下v8是如何运行代码的

image.png

js是一门解释型的语言,而解释型的语言特点就是启动快,执行慢,像c/c++这种编译型的语言特点则是,启动慢,执行快。v8引擎则是结合了两者的优点:

首先如上图所示,一段js代码通过解析生成抽象语法树,然后生成字节码,解释器可以直接解释执行字节码,或者通过编译器将其编译为二进制的机器代码再执行,首先是通过解释器执行并且输出结果,随后如果有一段代码被反复的执行,v8会启动Turbofan,将字节码文件编译成机器码,从而加快js的执行效率。

我们来看一个具体的例子:

const { performance, PerformanceObserver } = require('perf_hooks');

const add = (a, b) => a + b;
const num1 = 1;
const num2 = 2;

performance.mark('start');

for (let i = 0; i < 10000000; i++) {
    add(num1, num2);
}

// add(num1, 'x');

for (let i = 0; i < 10000000; i++) {
    add(num1, num2)
}

performance.mark('end');

const observer = new PerformanceObserver((list) => {
    console.log(list.getEntries()[0]);
})

observer.observe({ entryTypes: ['measure'] })

performance.measure('test', 'start', 'end');
复制代码

如上一段代码,他的执行时间是:

15ms

image.png

输入命令node --trace-opt v8-template1.js,观察一下他的优化过程

image.png

可以看出add 这个方法已经被Turbofan 优化了,原因是因为这是一段高频且稳定的代码。

接着,我们把上面注释掉的 add(num1, 'x'); 放开,然后再跑一次

image.png

发现执行时间变长了,why?

输入命令node --trace-deopt v8-template1.js,观察下他的反优化过程

image.png

清晰的看到,上面反优化的原因是因为入参的反馈类型不足,其实就是,一直都是两个数字相加,突然有一个变成了字符串,产生了不确定性v8不知道该怎么优化了,也就是对应上图中绿色的DeOptimize的步骤。

v8 的源码中详细的列出了会导致反优化的场景:github.com/v8/v8/blob/…

还有一篇对应的不错的解释: Optimization killers,阔以研究一下。

网络传输相关问题

image.png

启用http2

上文说了,http2有两个很大的优势是:

  1. 多路复用,解除了浏览器最大链接数的限制
  2. 服务端推送,节省了TTFB的时间

我们来实际观察一下:

http1: image.png

http2:

image.png

服务端推送:

正常的http请求,都会有一个从请求发出到接收到首字节的时间,这个就是TTFB,而http2服务端推送节省了这一时间(源码以上传: github.com/ycvcb123/pe… 可以自己观察下。

正常请求: image.png

push:

image.png

自动化性能检测工具介绍?

PUPPETEER + LIGHTHOUSE

源码以上传: github.com/ycvcb123/pe…

puppeteer是谷歌提供的一个无头浏览器(在无界面的情况下操作浏览器)

lighthouse 谷歌官方提供了node版的使用方式

大概的原理就是,通过puppeteer启动浏览器,解决一些登录态的问题,然后通过lighthouse检测页面性能,获取分析结果,自定义分析展示。

image.png

点击查看详情,可以看到lighthouse 的具体分析报告

image.png

 总结:

   关于性能优化的方案和知识点还有很多很多,对于不同的项目侧重点可能也不一样,日后也会慢慢补充进来,这方面的工作需要静下心来慢慢分析,多多尝试,某位伟人说过,耐心是一切聪明才智的基础,希望你我都可以在平时繁忙的需求中,可以抽一些时间出来,多一些沉淀,多一些进步。

   回想上一次在掘金写文章,居然还是2019年,2年多的时间,其实有很多可以记录的知识点,但是因为懒惰,没能写下来,以至于在跟别人讨论问题的某些时候,明明有一块的知识我曾经特别熟悉,但却不是能说的太清楚了,想翻下以前的记录,却发现一个字都没有。。。(像极了萧亚轩的那首《最熟悉的陌生人》),然后只能弱弱的说句:我以前研究过,不太记得了~,所以有时间还是尽量做一些记录,于人于己,百利无害。好习惯还是要捡起来!

未完待续...我们下次见 ヾ( ̄▽ ̄)ByeBye

文章分类
前端