性能优化是个大课题,涉及到了网络、操作系统、Web等一系列知识,每个环节都可以借助相关技术⼿段来进行优化,所以系统性地围绕一个web页面从请求到展示,到交互的全过程每个环节的知识点去学习,去思考可以每个环节可以做什么优化,然后借助各类网页指标分析去衡量页面性能,那么我们也一定能开发出性能优异的页面,同时面试的时候,与性能相关的问题也是能手到擒来。
关于性能优化
什么是前端性能优化?
前端性能是指⻚⾯信息加⼯(⽐如数据展现、动画、操作效率等)的效率。
优化是指借助相关技术⼿段提⾼这样的效率。
为什么前端性能如此重要?
我们知道,现在就是一个"流量为王"的时代,一个网站最重要的的就是用户,有了用户你才能有业务,打比方,你是一个电商网站,那么你肯定希望你的用户越多越好,这样才会有更多的人去浏览你的商品,从而在你的网站上花钱,买东西,这样你才能产生收益,但假如你的网站打开要十几秒,请求接口要十几秒,那用户还愿意等么?
看一下以下的用户体验图:
国外一些著名公司的调研:
BBC页⾯加载时长每增加1秒,⽤户流失10%
Pinterest减少页⾯加载时长40%, 提⾼了搜索和注册数15%
DoubleClick发现如果移动⽹站加载时长超过3秒,53%的⽤户会放弃
所以说,做好性能优化,提高用户体验很重要!
网页性能指标及影响因素
什么是网页性能指标?
既然前端页面性能如此之重要,也是用户体验的核心衡量标准。我们想要让网站的性能提升,具体指的是什么?如何去量化我们的优化?而性能体验这种东西往往是相对的,所以在讨论性能时,我们需要有精确的数据,需要有可以测量的客观的标准来定义,这些标准就是网页性能****指标。
Timing
页面运行的时间线(统计了从浏览器从网址开始导航到 window.onload
事件触发的一系列关键的时间点):
各个时间点的含义:
开始导航
- startTime:即navigationStart, 导航开始的时间
Prompt for unload(上一个页面卸载)
- unloadEventStart:表示窗口中的前一个网页(与当前页面同域)unload的时间戳。如果没有前一个网页,或者前一个网页和当前页面不是同域,则返回值为0。
- unloadEventEnd:表示当unload事件结束时的时间戳。 如果没有前一个网页,或者前一个网页和当前页面不是同域,则返回值为0。
Redirect(重定向)
- redirectStart:表示当第一个HTTP重定向开始时的时间戳。如果没有重定向,或者其中一个重定向不是同域,则返回值为0。
- redirectEnd:表示当最后一个HTTP重定向完成时,即接收到HTTP响应的最后一个字节时的时间戳。如果没有重定向,或者其中一个重定向不是同域,则返回值为0。
- fetchStart:表示当浏览器准备好使用HTTP请求获取文档时的时间戳。这个时刻是发生在检查任何应用程序缓存之前。
DNS (DNS解析)
- domainLookupStart:表示当DNS域名查询开始时的时间戳。如果使用了持久连接,或者信息存储在缓存或本地资源中(即无DNS查询),则该值将与fetchStart相同。
- domainLookupEnd:表示当DNS域名查询完成时的时间戳。如果使用了持久连接,或者信息存储在缓存或本地资源中(即无DNS查询),则该值将与fetchStart相同。
TCP 三次握手
- connectStart:表示HTTP TCP开始建立连接的时间戳。如果传输层报告了一个错误,并且重新开始建立连接,则给出最后一次建立连接的开始时间戳。如果使用持久连接,则该值与fetchStart相同。
- secureConnectionStart:表示当安全连接握手(HTTPS连接)开始时的时间戳。如果没有安全连接,则返回0。
- connectEnd:表示HTTP TCP完成建立连接(完成握手)的时间戳。如果传输层报告了一个错误,并且重新开始建立连接,则给出最后建立连接的结束时间。如果使用持久连接,则该值与fetchStart相同。当所有安全连接握手或SOCKS身份验证都被终止时,该连接被视为已打开。
Request(请求数据)
- requestStart:表示浏览器发送请求从服务器或本地缓存中获取实际文档的时间戳。如果传输层在请求开始后失败,并且连接重新打开,则此属性将被设置为与新请求对应的时间。
- responseStart:表示当浏览器从服务器的缓存或本地资源接收到响应的第一个字节时的时间戳(⾸字节时间)。
- responseEnd:表示当浏览器从服务器、缓存或本地资源接收到响应的最后一个字节时或者当连接被关闭时(如果这是首先发生的)的时间戳。
processing, JS 执行、 DOM 解析渲染
- domInteractive:表示解析器完成解析dom树的时间戳,这时document.readyState变为'interactive',相应的readystatechange事件被抛出。这时候只是解析完成DOM树,还没开始加载网页内的资源(可交互时间 ,测试点 )。
- domContentLoadedEventStart:表示DOM解析完成后,网页内的资源开始加载的时间戳。就在解析器发送DOMContentLoaded事件之前。
- domContentLoadedEventEnd:表示DOM解析完成后,网页内的资源加载完成的时间戳。即在所有需要尽快执行的脚本(按顺序或不按顺序)被执行之后。
- domComplete:表示当解析器完成它在主文档上的工作时,也就是DOM解析完成,且资源也准备就绪的时间。document.readyState变为'complete',相应的readystatechange事件被抛出。
load事件开始
-
loadEventStart:表示当为当前文档发送load事件时,也就是load回调函数开始执行的时间。如果这个事件还没有被发送,它将返回0。
-
loadEventEnd:表示当load事件的回调函数执行完毕的时间,即加载事件完成时。如果这个事件还没有被发送,或者还没有完成,它将返回0(完全加载时间,测试点 )。
通过时间线上的各个时间点,可以得到的性能指标有:
css
// performance.timing
console.table([['DNS解析耗时', performance.timing.domainLookupEnd - performance.timing.domainLookupStart],
['TCP连接耗时', performance.timing.connectEnd - performance.timing.connectStart],
['请求耗时', performance.timing.responseEnd - performance.timing.requestStart],
['解析DOM树耗时', performance.timing.domComplete - performance.timing.domInteractive],
['domready时间', performance.timing.domContentLoadedEventEnd - performance.timing.fetchStart],
['onload时间', performance.timing.loadEventEnd - performance.timing.fetchStart]
]);
关于Performance API
Performance API 是一组用于衡量 web 应用性能的标准接口,学习链接:developer.mozilla.org/zh-CN/docs/...
常用Performance API :
- performance.timing可以获取网页运行过程中每个时间点对应的时间戳(绝对时间,ms),但却即将废弃
- performance.getEntries(),以对象数组的方式返回所有资源的数据,包括css,img,script,xmlhttprequest,link等等
- performance.getEntriesByType( :string),和上面的 getEntries 方法类似,不过是多了一层类型的筛选,常见性能类型可以有 navigation(页面导航)、resource(资源加载)、paint(绘制指标)等
arduino
// 页面导航时间
performance.getEntriesByType('navigation')
// 静态资源
performance.getEntriesByType('resource')
// 绘制指标
performance.getEntriesByType('paint')
/*需要定时轮询, 才能持续获取性能指标*/
- performance.getEntriesByName(name: string, type?:string),理,和上面的 getEntries 方法类似,多了一层名字的筛选,也可以传第二个参数再加一层类型的筛选
rust
performance.getEntriesByName('https://i0.hdslb.com/bfs/svg-next/BDC/danmu_square_line/v1.json')
performance.getEntriesByName('https://cloud.tencent.com/developer/api/user/session')
/*需要定时轮询, 才能持续获取性能指标*/
- performance.now(),返回当前时间与performance.timing.navigationStart的时间差
arduino
console.log(performance.now())
// 5483324.099999994
- PerformanceObserver (观察者模式)推荐, 主要用于监测性能度量事件
php
/* 写法一 */
//直接往 PerformanceObserver() 入参匿名回调函数,成功 new 了一个 PerformanceObserver 类的,名为 observer 的对象
var observer = new PerformanceObserver(function (list, obj) {
var entries = list.getEntries();
for (var i = 0; i < entries.length; i++) {
//处理"navigation"和"resource"事件
}
});
//调用 observer 对象的 observe() 方法
observer.observe({ entryTypes: ["navigation", "resource"] });
/* 写法二 */
//预先声明回调函数 perf_observer
function perf_observer(list, observer) {
//处理"navigation"事件
}
//再将其传入 PerformanceObserver(),成功 new 了一个 PerformanceObserver 类的,名为 observer2 的对象
var observer2 = new PerformanceObserver(perf_observer);
//调用 observer2 对象的 observe() 方法
observer2.observe({ entryTypes: ["navigation"] });
// 案例: index-observer.html
实例化PerformanceObserver 对象,observe方法的entryTypes主要性能类型有哪些?
lua
console.log(PerformanceObserver.supportedEntryTypes)
/*
['element', 'event', 'first-input', 'largest-contentful-paint', 'layout-shift',
'longtask', 'mark', 'measure', 'navigation', 'paint', 'resource', 'visibility-state']
*/
具体每个性能类型的含义:
element | 元素加载时间,实例项是 PerformanceElementTiming 对象。 |
---|---|
event | 事件延迟,实例项是 PerformanceEventTiming 对象。 |
first-input | 用户第一次与网站交互(即点击链接、点击按钮或使用自定义的 JavaScript 控件时)到浏览器实际能够响应该交互的时间,称之为 First input delay -- FID。 |
largest-contentful-paint | 屏幕上触发的最大绘制元素,实例项是 LargestContentfulPaint 对象。 |
layout-shift | 元素移动时候的布局稳定性,实例项是 LayoutShift对象。 |
long-animation-frame | 长动画关键帧。 |
longtask | 长任务实例,归属于 PerformanceLongTaskTiming 对象。 |
mark | 用户自定义的性能标记。实例项是 PerformanceMark 对象。 |
measure | 用户自定义的性能测量。实例项是 PerformanceMeasure 对象。 |
navigation | 页面导航出去的时间,实例项是 PerformancePaintTiming 对象。 |
paint | 页面加载时内容渲染的关键时刻(第一次绘制,第一次有内容的绘制,实例项是 PerformancePaintTiming 对象。 |
resource | 页面中资源的加载时间信息,实例项是 PerformanceResourceTiming 对象。 |
visibility-state | 页面可见性状态更改的时间,即选项卡何时从前台更改为后台,反之亦然。实例项是 VisibilityStateEntry 对象。 |
soft-navigation | - |
用户为导向性能指标介绍
首次绘制(First Paint)和首次内容绘制(First Contentful Paint)
首次绘制(FP)和首次内容绘制(FCP)。在浏览器导航并渲染出像素点后,这些性能指标点立即被标记。 这些点对于用户而言十分重要,直乎感官体验!
首次绘制(FP),首次渲染的时间点。FP和FCP有点像,但FP一定先于FCP发生,例如一个页面加载时,第一个DOM还没绘制完成,但是可能这时页面的背景颜色已经出来了,这时FP指标就被记录下来了。而FCP会在页面绘制完第一个 DOM 内容后记录。
首次内容绘制(FCP),首次内容绘制的时间,指页面从开始加载到页面内容的任何部分在屏幕上完成渲染的时间。
javascript
/* PerformanceObserver监控 */
const observer = new PerformanceObserver((list) => {
const entries = list.getEntries();
entries.forEach((entry) => {
if (entry.name === 'first-paint') {
console.log('FP(首次绘制):', entry.startTime);
} else if (entry.name === 'first-contentful-paint') {
console.log('FCP(首次内容绘制):', entry.startTime);
}
});
});
observer.observe({ entryTypes: ['paint'] });
// 案例: index-observer.html
/* performance.getEntriesByName*/
console.log(
"FP(首次绘制):" + performance.getEntriesByName("first-paint")[0].startTime
);
console.log(
"FCP(首次内容绘制):" +
performance.getEntriesByName("first-contentful-paint")[0].startTime
);
首次有效绘制(First Meaningful Paint)
有效内容,这种一般很难清晰地界定哪些元素的加载是「有用」的(因此目前尚无规范),但对于开发者他们自己而言,他们更知道页面的哪些部分对于用户而言是最为有用的,所以这样的衡量标准更多的时候是掌握在开发者手上!
ini
const observer = new PerformanceObserver((list) => {
const entries = list.getEntries();
entries.forEach((entry) => {
if (entry.name === 'https://xxxxxx.xxx.jpg') {
console.log(entry.startTime);
}
});
});
observer.observe({ entryTypes: ['resource'] }); // 可以是图片、某个Dom元素
可交互时间(TTI)
指标测量页面从开始加载(FCP)到主要子资源完成渲染,并能够快速、可靠地响应用户输入所需的时间。阻塞会影响正常可交互的时间,浏览器主线程一次只能处理一个任务, 如果主线程长时间被占用,那么可交互时间也会变长,所以更多的TTI都是发生在主线程处于空闲的时间点
良好的TTI应该控制在5秒以内。
测量TTI
的最佳方法是在网站上运行Lighthouse性能审核
arduino
console.log(performance.timing.domInteractive); // 可交互时间点
长任务(Long Task)
浏览器主线程一次只能处理一个任务。 某些情况下,一些任务将可能会花费很长的时间来执行,持续占用主进程资源,如果这种情况发生了,主线程阻塞,剩下的任务只能在队列中等待。
用户所感知到的可能是输入的延迟,或者是哐当一下全部出现。这些是当今网页糟糕体验的主要来源之一。
Long Tasks API认为任何超过50毫秒的任务(Task)都可能存在潜在的问题,并将这些任务相关信息回调给给前端。
把 long task 时间定义为 50ms 的主要理论依据是 Chrome 提出的 RAIL 模型,RAIL 认为事件响应应该在 100ms 以内,滚动和动画处理应该在 16ms 以内,才能保证好的用户体验,而如果一个 task 执行超过 50ms,则很有可能让体验达不到 RAIL 的标准,故我们需要重点关注执行时间超过 50ms 的任务。
javascript
const observer = new PerformanceObserver((list) => {
const entries = list.getEntries();
entries.forEach((entry) => {
console.log('Long Task(长任务):', entry);
});
});
observer.observe({ entryTypes: ['longtask'] });
// 案例: index-longTask.html
Core Web Vitals(核心网页指标)
什么是Core Web Vitals?
从上一小节学习,可以知道,衡量一个 Web
页面的体验和质量有着非常多的指标,我们又不是性能专家,所以为了简化场景,帮助网站专注于最重要的指标,所以在新一代的谷歌浏览器官方推出了核心 Web 指标( LCP ****FID ****CLS ) ,协助我们能够有更好的聚焦关注度和降低我们得理解成本,那具体有哪些呢?
- Largest Contentful Paint (LCP) :最大内容绘制, 用于衡量加载性能。 为了提供良好的用户体验,LCP 应在网页首次开始加载后的 2.5 秒内发生。
- First Input Delay (FID) :首次输入延迟,用于衡量可交互性。为了提供良好的用户体验,页面的 FID 应不超过 100 毫秒。
- Cumulative Layout Shift (CLS) :累积布局偏移,用于衡量视觉稳定性。为了提供良好的用户体验,页面应保持 0.1 或更低的 CLS
LCP: Largest Contentful Paint 最大内容绘制
最大内容绘制 ( LCP ) 指标会根据页面首次开始加载(FCP)的时间点来报告可视区域内可见的最大图像或文本块(、、 url ()) 完成渲染的相对时间。
通常Web 页面是分阶段加载的,所以,页面上最大的元素可能会发生变化。例如上图,在一个带有文本和图像的页面上,浏览器最初可能只是呈现文本,而此时浏览器会分派一个性能类型(entryType)为largest-contentful-paint 的entry。稍后,图像完成加载完成,会分派第二个性能类型(entryType)为largest-contentful-paint 的entry。
测量方式:
javascript
const observer = new PerformanceObserver((list) => {
const entries = list.getEntries();
entries.forEach((entry) => {
console.log('LCP(最大内容绘制):', entry.startTime);
});
});
observer.observe({ entryTypes: ['largest-contentful-paint'] });
// 案例: index-LCP.html
FID:First Input Delay 首次输入延迟
FID( First Input Delay)
测量从用户第一次与页面交互,例如当他们单击链接、点按按钮或使用由 JavaScript 驱动的自定义控件,直到浏览器对交互作出响应,并实际能够开始处理事件处理程序所经过的时间。
由此图可以看出,FID值越小越好,FID 是发生在 FCP 和 TTI 之间,这个阶段虽然页面已经显示出部分内容,却不具备完全的可交互性。这个阶段用户和页面交互,往往会有较大延迟。如下图所示,浏览器接收到用户输入操作时,主线程正在忙于执行一个 Long Task,只有当这个 Task 执行完成后,浏览器才能响应用户的输入操作。
测量方式:
arduino
//FID 的计算需要用户真实操作页面,可以创建 PerformanceObserver 对象,监听 *first-input* 事件,
// 监听到 *first-input* 事件后,然后通过事件的开始处理时间,减去事件的发生时间,即为 FID。
new PerformanceObserver((entryList) => {
for (const entry of entryList.getEntries()) {
const delay = entry.processingStart - entry.startTime;
console.log('FID 首次输入延迟:', delay, entry);
}
}).observe({entryTypes: ['first-input']});
// 案例: index-FID.html
CLS:Cumulative Layout Shift 累积布局偏移
CLS 测量整个页面生命周期内发生的所有意外布局偏移量中最大一连串的布局偏移分数。每当一个可见元素的位置从一个已渲染帧变更到下一个已渲染帧时,就发生了布局偏移 。
简单地说,你是否曾经历过在网上阅读一篇文章,结果页面上的某些内容突然发生改变?文本在毫无预警的情况下移位,导致您找不到先前阅读的位置。或者更糟糕的情况:您正要点击一个链接或一个按钮,但在你手指落下的瞬间,诶?链接移位了,结果点到了别的东西,这个就是意外偏移。
是怎么计算出来的呢?
erlang
布局偏移分数 = 影响比例 * 距离分数
影响比例,比如上图文字块(不稳定元素)占可视区域50%,出现意外布局后,向下偏移20%,那么我们求并集,50% + 20% = 70%
距离分数,因为向下25%,那么距离分数就是25%
布局偏移分数, 0.7 * 0.25 = 0.1875
70% * 25%
如何衡量的呢?
ini
let clsValue = 0;
let clsEntries = [];
let sessionValue = 0;
let sessionEntries = [];
new PerformanceObserver((entryList) => {
for (const entry of entryList.getEntries()) {
// 仅计算最近没有用户输入的布局变化。
if (!entry.hadRecentInput) {
const firstSessionEntry = sessionEntries[0];
const lastSessionEntry = sessionEntries[sessionEntries.length - 1];
// 如果当前entry发生在前一个entry之后不到 1 秒且第一个entry之后不到 5 秒,
// 那么会对entry.value进行累加。否则,重新赋值
// 注: 我们把这个时间区间范围内发生的偏移进行累计,称做一个会话窗口,
// 对layout-shift收集到的entry进行清洗,得到最大的CLS分数
if (sessionValue
&& entry.startTime - lastSessionEntry.startTime < 1000
&& entry.startTime - firstSessionEntry.startTime < 5000) {
sessionValue += entry.value;
sessionEntries.push(entry);
} else {
sessionValue = entry.value;
sessionEntries = [entry];
}
// 如果当前sessionValue大于当前CLS值
// 更新 CLS。
if (sessionValue > clsValue) {
clsValue = sessionValue;
clsEntries = sessionEntries;
// Log the updated value (and its entries) to the console.
console.log('Cumulative Layout Shift 累积布局偏移:', clsValue, clsEntries)
}
}
}}).observe({ entryTypes: ['layout-shift'] });
// 性能类型layout-shift用于测量JavaScript 中的布局偏移
// 案例: index-CLS.html
Performance使用及分析
Performance是Google浏览器自带的页面性能插件,可以记录站点在运行过程中的性能数据,有了这些性能数据,我们就可以回放整个页面的执行过程,这样就方便我们来定位和诊断每个时间段内页面的运行情况,从而有效帮助我们找出页面的性能瓶颈。
Performance配置面板
生成的报告页
- 时间线:概览面板和性能面板都是依赖于时间线的,假如我们录制了10000毫秒,那么它们的时间线就是10000毫秒
- 概览面板 :由
页面帧速 (FPS)、CPU 资源消耗、网络请求流量、V8 内存使用量 (堆内存)
几项指标按照时间顺序记录变化的面板,FPS 图表上出现了红色块,那么就表示红色块附近渲染出一帧所需时间过久,帧的渲染时间过久,就有可能导致页面卡顿,你可以点击该红色块,那就可以把时间线聚焦到该问题区域。 - 性能面板:概览面板是用来定位到可能存在问题的时间节点,如果需要更进一步的数据,来分析导致性能问题的原因,需要从性能面板入手。我们介绍一下有哪些常见性能指标项:
1、Network指标:该指标展示了页面中的每个网络请求所消耗的时长,并以瀑布流的形式展现。
2、Frames 指标: 也就是浏览器生成每帧的记录,我们知道页面所展现出来的画面都是由渲染进程一帧一帧渲染出来的,帧记录就是用来记录渲染进程生成所有帧信息,包括了渲染出每帧的时长、每帧的图层构造等信息,你可以点击对应的帧,然后在详细信息面板里面查看具体信息。
3、Timings 指标:用来记录一些关键的时间节点在何时产生的数据信息,像FP、LCP、DOMContentLoaded、Onload 等事件产生的时间点,它们体现在在几条不同颜色的竖线上。
4、Main 指标 *: *记录渲染主线程的任务执行过程,大部分流程、Jacvascirpt执行、V8垃圾回收、定时设置回调任务等等也是均跑在主线程的, 所以这也是我们最需要关注的一个指标。
5、Raster 指标: ******合成线程维护了一个光栅化线程池 (Raster),用来让 GPU 执行光栅化的任务,如果你点开 Raster 项,可以看到它维护了多个线程。
6、Compositor 指标:记录了合成线程的任务执行过程
7、GPU 指标: 记录了 GPU 进程主线程的任务执行过程
8、Chrome_ChildIOThread ****指标 : 渲染进程维护着一个IO线程,主要用来接收用户输入事件、网络事件、设备相关等事件,如果事件需要渲染主线程来处理,那么 IO 线程还会将这些事件转发给渲染主线程。在性能面板上,Chrome_ChildIOThread
指标对应的就是 IO 线程的任务记录。
9、Layout Shifts,页面加载过程中发生的视觉不稳定性
10、ThreadPoolForegroundWorker、ThreadPoolServiceWorker
11、 ...
Main指标分析
任务与过程
Main 指标就记录渲染主线上所执行的全部任务 ,以及每个任务的详细执行过程,所以了解任务和过程在Performance面板的体现是很有必要的。
可以打开 Chrome 的开发者工具,选择 Performance 标签,然后录制加载阶段任务执行记录,然后关注 Main 指标,进行截取,如下图示:
观察上图,这是对Main指标某个Task的截图, 可以观察到,图上方有一段灰色横条,该灰色横条就对应了一个任务,灰色长条的长度对应了任务的执行时长
。但是,渲染主线程上的任务都是比较复杂的,如果只单纯记录任务执行的时长,一般很难定位问题,所以,还需要将任务执行过程中的一些关键的细节记录下来,这些细节就是任务的过程 ,灰线下面的横条就是一个个过程
,同样这些横条的长度就代表这些过程执行的时长。
我们可以把任务看成是一个 Task 函数,在执行 Task 函数的过程中,它会调用一系列的子函数,这些子函数就是我们所提到的过程。
分析页面加载过程
我们来分析一个简单的页面,代码如下所示:
javascript
<html>
<head>
<title>Main指标分析</title>
<style>
.area {
border: 2px black solid;
}
.block {
background-color: green;
height: 100px;
margin: 10px;
width: 100px;
}
</style>
</head>
<body>
<div class="area">
<div class="block"></div>
</div> <br>
<script>
function setBlockArea() {
var el = document.createElement('div');
el.setAttribute('class', 'area');
el.innerHTML = '<div class="block"></div>';
document.body.append(el);
}
setBlockArea()
</script>
</body>
</html>
// 完成一次绘制需要执行
// profiling overhead => 分析开销
// Schedule Style Recalculation => 调度样式重新计算准备
// Recalculate Style 重新计算样式
// Layout 布局
// Pre-Paint 准备绘制
// Paint 绘制
// Composite Layers 合成线程进行工作
// Layerize 将显示列表分解成一个合成的图层列表(composited layer list ),用于独立的栅格化(rasterization)和动画制作
// 案例: index-main.html
通过前面几小节,我们了解了常用网页性能指标、核心性能指标的一些概念,同时了解如何使用Performance API对各个指标进行性能衡量,我们知道,如果我们想最终在浏览器显示我们所期望的页面和交互效果,那我们首先需要的是我们应用代码、资源、脚本一切准备好,才后续页面的渲染和展示,于是缩短加载的时间,也是我们性能优化的表现,下一篇围绕着如何缩短加载时间手段出发,去了解浏览器工作原理,常用框架优化策略,以及扩展知识PWA的一些应用!