前端性能优化(四)(传输优化)

Front-End Performance Checklist 2021[1]
https://www.smashingmagazine....
前端性能优化(一):准备工作[2]
前端性能优化(二):资源优化[3]
前端性能优化(三):构建优化[4]
一、使用defer异步加载关键Java Script defer:异步加载,当HTML解析完毕后才执行。
async:异步加载,脚本下载完成后立即执行(脚本准本好,且之前的所有同步工作也执行完的时候执行)。如果脚本下载较快,比如直接从缓存获取,会阻塞HTML解析。同时,多个async脚本执行顺序不可预料。推荐使用defer。
不推荐同时使用defer和async,async的优先级高于defer。
前端性能优化(四)(传输优化)
文章图片

二、使用 IntersectionObserver 和优先级提示(priority hints)懒加载耗性能的组件 Native lazy loading(only Chromium)已经对images和iframes可用,在DOM上添加loading属性即可。当元素距离可视窗口一定距离时才加载。该阈值取决于几件事,从正在获取的图像资源的类型到网络连接类型。使用Android上的Chrome浏览器进行的实验表明,在4G上,延迟可见的97.5%的折叠后图像在可见后的10ms内已完全加载。即使在速度较慢的2G网络上,在10毫秒内仍可以完全加载92.6%的折叠图像。截至2020年7月,Chrome进行了重大改进,以对齐图像延迟加载的视口距离阈值,以更好地满足开发人员的期望。在网络情况比较好的情况下(如:4g),distance-from-viewport thresholds为1250px,在网络情况比较差的情况下(如:3g),距离阀值设为2500px。
实现lazy loading最好的方式就是使用Intersection Observer API。它提供异步检测元素是否在祖先元素或根元素(通常是滚动的父元素)可见,我们可异步控制操作。
为兼容所有浏览器,我们使用Hybrid Lazy Loading[5]With IntersectionObserver[6]。
前端性能优化(四)(传输优化)
文章图片

想了解更多关于懒加载的可以阅读Google的Fast load times[7]。
另外,我们可以在DOM节点上使用important属性[8],重置资源的优先级。它可以用
值得注意的是,动态样式也可能很昂贵,但通常仅在您依赖于数百个同时渲染的合成组件的情况下。因此,如果使用的是CSS-in-JS,请确保你的CSS-in-JS库在CSS不依赖主题或props并且不过度组合样式化组件的情况下优化执行。有兴趣的可以阅读 Aggelos Arvanitakis的The unseen performance costs of modern CSS-in-JS libraries in React apps[14]。
八、考虑让组件具有可连接性 如果你的网站允许用户以Save-Data的模式访问,当用户开启时,以请求头的形式传递给服务端,服务端传输较少的内容回来。虽然它本身不做任何事情,但是服务提供商或网站所有者可以根据该头信息进行相应的处理:
● Google Chrome浏览器可能会强制执行干预措施,例如推迟外部脚本以及延迟加载iframe和图像 ● Google Chrome可以代理请求,以提高选择加入“精简版”且网络连接状况不佳的用户的性能 ● 网站所有者可以提供其应用程序的较轻版本,例如 通过降低图像质量,交付服务器端呈现的页面或减少第三方内容的数量 ● ISP可以转换HTTP图像以减小最终图像的大小

当然,除了用户主动开启,作为开发,我们也可以根据用户当前的网络状态去判断是否给用户返回“精减版”内容。使用Network Information API即可获得,取值有:slow-2g, 2g, 3g, or 4g。
navigator.connection.effectiveType

为了更方便控制,我们还可以借助service worker拦截。
"use strict"; self.addEventListener('fetch', function (event) { // Check if the current request is 2G or slow 2G if (/\slow-2g|2g/.test(navigator.connection.effectiveType)) { // Check if the request is for an image if (/\.jpg$|.png$|.gif$|.webp$/.test(event.request.url)) { // Return no images event.respondWith( fetch('placeholder.svg', { mode: 'no-cors' }) ); } } });

九、考虑让你的组件对设备内存敏感 除了网络状态,设备的内存情况我们也应该考虑到。使用Device Memory API,即navigator.deviceMemory,可以得到设备拥有多少RAM(以GB为单位),四舍五入到最接近2的幂。
十、预热连接以加速传输 有几个资源提示你需要了解:
● dns-prefetch:在后台执行DNS查找 ● preconnect:要求浏览器在后台启动连接握手(DNS,TCP,TLS) ● prefetch:要求浏览器请求资源 ● preload:在不执行资源的情况下预取资源 ● prerender:提示浏览器在后台为下一个导航构建整个页面的资源(已被弃用:从巨大的内存占用和带宽使用到多个注册的分析点击率和广告曝光量等,都很有挑战。) ● NoState Prefetch:像 prerender 一样,NoState Prefetch 会提前获取资源;但不同的是,它不执行 JavaScript,也不提前渲染页面的任何部分

这里主要介绍preload和prefetch, vs ,更多关于preload和prefetch,可阅读Loading Priorities in Chrome(还详细介绍了什么情况下可能会导致请求两次等等)
● preload是一种声明性提取,可强制浏览器对资源进行请求,而不会阻止document的onload事件;prefetch是向浏览器提示可能需要资源的提示,浏览器决定是否以及何时加载该资源。 ● preload通常是你具有高度自信预加载的资源将在当前页面中使用;prefetch通常是可能用于跨多个导航边界的未来导航的资源。 ● preload是对浏览器早期的获取指令,用于请求页面所需的资源(关键脚本,Web字体,hero图像);prefetch使用情况略有不同-用户将来的导航(例如,在视图或页面之间),其中所获取的资源和请求需要在导航之间保持不变。如果页面A发起了对页面B所需的关键资源的预取请求,则可以并行完成关键资源和导航请求。如果我们在此用例中使用preload,则会在页面A的卸载后立即取消。 ● 浏览器有4种缓存:HTTP cache, memory cache, Service Worker cache & Push cache。preload和prefetch都是存在HTTP cache中。

有兴趣的同学还可以看一下Early Hints、Priority Hints。
十一、使用 service worker 做性能优化 前面我们也看到了很多地方都有使用service worker,这里我们详细介绍一下。
Service Worker 是浏览器在后台独立于网页运行的脚本,核心功能是拦截和处理网络请求,包括通过程序来管理缓存中的响应。它可以支持离线体验。
(一)注意事项
● 它是一种 JavaScript Worker,无法直接访问 DOM、LocalStorage、window。ServiceWorker 通过响应 postMessage 接口发送的消息来与其控制的页面通信,页面可在必要时对 DOM 执行操作。 ● Service Worker 是一种可编程网络代理,让你能够控制页面所发送网络请求的处理方式。 ● Service Worker 在不用时会被中止,并在下次有需要时重启,因此,你不能依赖 Service Worker onfetch 和 onmessage 处理程序中的全局状态。如果存在你需要持续保存并在重启后加以重用的信息,Service Worker 可以访问 IndexedDB API。 ● Service Worker 广泛地利用了 promise。

(二)生命周期
Service Worker 的生命周期完全独立于网页,如下:
● 注册。 ● 安装:可缓存某些静态资产,如果所有文件均已成功缓存,那么 Service Worker 就安装完毕。如果任何文件下载失败或缓存失败,那么安装步骤将会失败,Service Worker 就无法激活(也就是说, 不会安装)。 ● 激活:是管理旧缓存的绝佳机会。 ● 控制:Service Worker 将会对其作用域内的所有页面实施控制,不过,首次注册该 Service Worker 的页面需要再次加载才会受其控制,线程实施控制后,它将处于以下两种状态之一: 服务工作线程终止以节省内存,或处理获取和消息事件,从页面发出网络请求或消息后将会出现后一种状态。

前端性能优化(四)(传输优化)
文章图片

(三)先决条件
● Service Worker 受 Chrome、Firefox 和 Opera 支持。 ● 在开发过程中,可以通过 localhost 使用 Service Worker,但如果要在网站上部署 Service Worker,则需要在服务器上设置 HTTPS。

(四)使用
// 1、注册(注册w完成后,你可以通过转至 chrome://inspect/#service-workers 并寻找你的网站来检查 Service Worker 是否已启用。) if ('serviceWorker' in navigator) { window.addEventListener('load', function() { // 这里/sw.js位于根网域。这意味着服务工作线程的作用域将是整个来源。 // 换句话说,Service Worker 将接收此网域上所有事项的 fetch 事件。 // 如果我们在 /example/sw.js 处注册 Service Worker 文件,则 Service Worker 将只能看到网址以 /example/ 开头(即 /example/page1/、/example/page2/)的页面的 fetch 事件。 navigator.serviceWorker.register('/sw.js').then(function(registration) { // Registration was successful console.log('ServiceWorker registration successful with scope: ', registration.scope); }, function(err) { // registration failed :( console.log('ServiceWorker registration failed: ', err); }); }); } // 2、安装(sw.js) self.addEventListener('install', function(event) { // 1、打开缓存 // 2、缓存文件 // 3、确认所有需要的资产是否已缓存 event.waitUntil( caches.open(CACHE_NAME) .then(function(cache) { // urlsToCache:文件数组 return cache.addAll(urlsToCache); }) .then(() => { // `skipWaiting()` forces the waiting ServiceWorker to become the // active ServiceWorker, triggering the `onactivate` event. // Together with `Clients.claim()` this allows a worker to take effect // immediately in the client(s). self.skipWaiting(); }) ); }); // 3、缓存与返回请求(sw.js) self.addEventListener('fetch', function(event) { event.respondWith( caches.match(event.request) .then(function(response) { // Cache hit - return response if (response) { return response; } // IMPORTANT:Clone the request. A request is a stream and // can only be consumed once. Since we are consuming this // once by cache and once by the browser for fetch, we need // to clone the response. var fetchRequest = event.request.clone(); return fetch(fetchRequest).then( function(response) { // Check if we received a valid response // 确保响应类型为 basic,亦即由自身发起的请求。 这意味着,对第三方资产的请求也不会添加到缓存。 if(!response || response.status !== 200 || response.type !== 'basic') { return response; }// IMPORTANT:Clone the response. A response is a stream // and because we want the browser to consume the response // as well as the cache consuming the response, we need // to clone it so we have two streams. // 克隆响应:这样做的原因在于,该响应是数据流, 因此主体只能使用一次。 var responseToCache = response.clone(); caches.open(CACHE_NAME) .then(function(cache) { cache.put(event.request, responseToCache); }); return response; } ); } ) ); });

注意:从无痕式窗口创建的任何注册和缓存在该窗口关闭后均将被清除。
(五)更新Service Worker
● 更新Service Worker, 需遵循以下步骤: ● 更新sw.js文件。用户访问你的网站时时,浏览器会尝试在后台重新下载定义 Service Worker 的脚本文件。如果 Service Worker 文件与其当前所用文件存在字节差异,则将其视为新 Service Worker。 ● 新 Service Worker 将会启动,且将会触发 install 事件。 ● 此时,旧 Service Worker 仍控制着当前页面,因此新 Service Worker 将进入 waiting 状态。 ● 当网站上当前打开的页面关闭时,旧 Service Worker 将会被终止,新 Service Worker 将会取得控制权。 ● 新 Service Worker 取得控制权后,将会触发其 activate 事件(activate 回调中常见任务是缓存管理。之所以需要缓存管理,是因为如果你在安装步骤中清除了任何旧缓存,则继续控制所有当前页面的任何旧 Service Worker 将突然无法从缓存中提供文件)。

// 遍历 Service Worker 中的所有缓存,并删除未在缓存白名单中定义的任何缓存(旧缓存)。 self.addEventListener('activate', function(event) { var cacheAllowlist = ['pages-cache-v1', 'blog-posts-cache-v1']; event.waitUntil( caches.keys().then(function(cacheNames) { return Promise.all( cacheNames.map(function(cacheName) { if (cacheAllowlist.indexOf(cacheName) === -1) { return caches.delete(cacheName); } }) ).then(() => { // `claim()` sets this worker as the active worker for all clients that // match the workers scope and triggers an `oncontrollerchange` event for // the clients. return self.clients.claim(); }); }) ); });

(六)可以做什么优化?
1、较小的HTML有效负载 将html打包成2个文件,首次访问的是有完整HTML的文件,访问完成后将文件的头和尾存储在缓存中,再次访问拦截请求,将其转发到只有内容的HTML文件,收到内容后将文件与之前存在在缓存的头和尾拼接(可以以流的形式)返回给浏览器。
// 这里使用workbox,若不想使用,具体可阅读(使用stream的形式返回html):https://livebook.manning.com/book/progressive-web-apps/chapter-10/55 // 另外,还需考虑页面的标题问题,这里就不叙述了,有兴趣的同学可以网上f翻阅资料 import {cacheNames} from 'workbox-core'; import {getCacheKeyForURL} from 'workbox-precaching'; import {registerRoute} from 'workbox-routing'; import {CacheFirst, StaleWhileRevalidate} from 'workbox-strategies'; import {strategy as composeStrategies} from 'workbox-streams'; const shellStrategy = new CacheFirst({cacheName: cacheNames.precache}); const contentStrategy = new StaleWhileRevalidate({cacheName: 'content'}); const navigationHandler = composeStrategies([ () => shellStrategy.handle({ request: new Request(getCacheKeyForURL('/shell-start.html')), }), ({url}) => contentStrategy.handle({ request: new Request(url.pathname + 'index.content.html'), }), () => shellStrategy.handle({ request: new Request(getCacheKeyForURL('/shell-end.html')), }), ]); registerRoute(({request}) => request.mode === 'navigate', navigationHandler);

2、离线缓存 本身就支持的功能
3、拦截替换资源 如拦截图像请求,如果请求失败,返回默认的失败图片
function isImage(fetchRequest) { return fetchRequest.method === "GET" && fetchRequest.destination === "image"; } self.addEventListener('fetch', (e) => { e.respondWith( fetch(e.request) .then((response) => { if (response.ok) return response; // User is online, but response was not ok if (isImage(e.request)) { // Get broken image placeholder from cache return caches.match("/broken.png"); } }) .catch((err) => { // User is probably offline if (isImage(e.request)) { // Get broken image placeholder from cache return caches.match("/broken.png"); } }) ) });

4、不同类型的资源使用不同的缓存策略 比如Network Only(live data)、Cache Only(Web font)、Network Falling Back to Cache(HTML, CSS, JavaScript, image)。
比如在支持webP格式图片的手机上返回格式为webP格式的图片等。
可以使用Request.destination区分不同的请求类型:请求相关的destination取值有:"audio", "audioworklet", "document", "embed", "font", "image", "manifest", "object", "paintworklet", "report", "script", "serviceworker", "sharedworker", "style", "track", "video", "worker", 或者 "xslt"。如果没有特别说明,则为空字符串。
5、还可以在CDN/Edge上使用 具体这里就不叙述了。
(七)当7 KB等于7 MB
DOMException: Quota exceeded.

如果你正在构建一个渐进式Web应用程序,并且当Service Worker缓存从CDN提供的静态资产时遇到过大的缓存存储,请确保为跨域资源设置正确的CORS响应标头,并且不要无意间与Service Worker缓存不透明的响应(opaque responses) ,你可以通过将crossorigin属性添加到前端性能优化(四)(传输优化)
文章图片

因此,页面加载应即时发生,用户应从给定的操作中获得立即反馈。
有一点需要记住:20%的规则。为了使用户看到优化前后的感知到的时间差异,必须将其至少更改20%。
与竞争对手相比,如果你的页面加载时长是5s,而竞争对手的是2s,就算你提升了20%也是不行的,这个时候就需要与竞争对手作比较。如果我们做不到优化到2s,那我们至少要优化到2s + 2 * 20% = 2.4s,这样至少用户不会感知到差异。
这里还有一个心理阀值。以刚刚的2s、5s为例,大于此阈值的持续时间将被用户感知为接近5秒。持续时间小于该阈值的持续时间将被视为接近2秒。通过这种概念,我们可以找到它的一个几何平均值:√(A × B),例子中就是:√(2 × 5) ≈ 3.2 seconds,如果加载时间小于3.2s,用户能注意到差异,但是这对他们来说此差异对他们如何选择服务并不重要。
我们可以将时间以用户的心理活动为特征去划分,划分为2个阶段:活动阶段或活动等待,这可能是一些物理活动或纯粹的思考过程,例如解决难题或在地图上找到方法;被动阶段或被动等待,这是用户无法选择或控制等待时间的时间段,例如排队或等待约会迟到的人。即使时间间隔在客观上是相等的,人们倾向于将被动等待的时间估计为比主动等待更长的时间。
我们常说的等待时间过长,通常说的是被动等待的时间。因此,为了管理心理时间并使大脑感知事件的持续时间少于实际时间,我们通常应通过增加事件的主动阶段来尽可能地减少事件的被动阶段。有多种技术可以实现这一目标,但是大多数可以归结为两个简单的实践:抢先启动和提早完成。
抢先启动 以活动阶段打开事件,并保持尽可能长的时间,然后再将用户切换到被动等待的过程。当然,这么做是也不要影响事件本身的时长。在很多人眼里,活动阶段不认为是等待时间。因此,对于用户的大脑而言,抢先式启动意味着将启动事件标记虚拟地移到更接近结束的位置(到活动阶段结束时),这将有助于用户感觉到事件变短了。
2009年在德克萨斯州休斯敦的机场面对不同寻常的投诉:旅客对到达目的地后提取行李的漫长等待感到不满意。机场立即做了改变,增加了行李处理员的数量,这将等待时间降至了8s,但是这并没有减少投诉。机场随后做了一系列调查发现,第一批行李花了大约八分钟时间出现在行李传送带上。但是,乘客仅需一分钟即可到达行李传送带。因此,平均而言,乘客要等7分钟才能看到第一批行李。从心理上来讲,主动阶段只有一分钟,而被动等待有七分钟。解决方案就是:将到达登机口远离主要航站楼,并将行李送至最远的传送带。这样,乘客的步行时间增加到了六分钟,而被动等待只剩下了两分钟。尽管走了更长的路,但投诉下降到几乎为零。
针对前端项目,我们也可以做类似的事。比如Safari的搜索功能,Safari会提前加载搜索列表的Top Hits的结果的页面,使得当用户点击此链接时可以更快的进入到页面。借用这种思想,我们可以利用资源提示实现抢先启动:dns-prefetch、preconnect、prefetch、preload、prerender。
提早完成 与我们可以在抢先启动技术中移动开始标记的方式类似,提早完成会将结束标记移到离开始更近的位置,从而使用户感觉过程正在迅速结束。在这种情况下,我们将以被动阶段打开事件,但是要尽快将用户切换到主动阶段。
在网络上最常使用此技术的地方是视频流服务。点击视频上的播放按钮时,无需等待整个视频的下载。当第一个最低要求的视频块可用时,开始播放。因此,结束标记移到更靠近起点的位置,并且为用户提供了有效的等待(观看下载的块),而其余视频则在后台下载。 简单有效。
在处理页面加载时间时,我们可以应用相同的技术。准备好要显示的基本要素(例如DOM)后,便开始渲染页面。如果资源不会影响渲染,我们就不必等待所有资源下载。我们甚至不需要所有HTML元素;我们可以稍后使用JavaScript注入那些不立即可见的内容,例如页脚。通过在开始时将加载分为短暂的被动等待,然后在加载并呈现初始信息后进行主动等待,尽快给用户一些东西,使他们认为该页面的加载速度比实际加载速度快。
除了以上这些,我们还可以做一些处理来提高用户的容忍度。
在20世纪上半叶,建筑经理收到了电梯等候时间长的投诉。经理们感到困惑,因为没有简单的技术解决方案。为了解决该问题,必须考虑一些昂贵且耗时的工程。这时,有人提出了从不同的非技术角度解决问题的想法:在电梯中安装镜子,并在大厅中安装落地镜。这很好的解决了该问题,为什么呢?该解决方案通过让人们互相注视自己(并暗中注视他人)来代替人们在主动电梯等待电梯时所经历的纯粹的被动等待。它并没有试图说服人们等待的时间缩短了,也没有对客观的时间或事件标记做任何事情。取而代之的是,人们从等待开始就一直过渡到活动阶段。这样,人们对等待的容忍度要大得多。
下面是一些等待心理的命题:
● P1:占用时间比空闲时间短。 ● P2:人们想开始使用(预处理等待比处理等待更长的时间)。 ● P3:焦虑使等待时间似乎更长。 ● P4:不确定的等待时间比已知的有限等待时间长。 ● P5:无法解释的等待时间比解释的等待时间长。 ● P6:不公平的等待要比公平的等待长。 ● P7:服务越有价值,客户等待的时间就越长。 ● P8:单独等待比集体等待更长。 ● P9:不舒服的等待比舒适的等待更长。 ● P10:新用户或不常使用的用户会觉得他们的等待时间比频繁用户要长。

解决P4和P5的问题,就是我们要说的容忍管理。首先,解决等待时间和进程状态的不确定性,我们可以使用良好的进度指示。进度指示又分为两类:动态(随着时间更新)、静态(一直不变的),其中每一个都可以再分为两个子类:确定的(按工作单位,时间或其他度量方式完成的项目)、不确定的(不完成项目)。
前端性能优化(四)(传输优化)
文章图片

如何选择呢?我们可以按之前的时间跨度去区分:
● 瞬间(0.1-0.2s):不需要 ● 即时(0.5-1s):1s是用户不间断思考的最长时间,显示复杂的指示将导致用户思考中断。通常情况下,显示没有文本信息的简单指示符没有什么害处。D类的指标,如旋转器或非常基本的进度条(简化的A类),可以避免打断用户的思维流程,同时巧妙地表明系统正在做出响应。 ● 最佳体验:在此时间段内,我们必须向用户指示输入或正在处理的请求以及系统正在响应。同样,最佳指标是D类指标或简化的A类指标–无需引起用户对其他信息的注意。 ● 注意力(5-10s):这是用户容忍阈值的前沿,需要更完整的解决方案。对于此间隔或更长时间内的事件,我们不仅需要显示一般的“正在处理”指示器,还需要显示更多信息:用户需要知道他们需要等待多长时间。因此,我们应该在过程的进展清晰的地方显示A或B类的动态指标。

前端性能优化(四)(传输优化)
文章图片

总的来说就是:
● 对于持续时间为0.5–1s的事件:建议根据情况建议显示D类(旋转器)或简化A类(进度条)的指示器。 ● 对于持续1到5秒的事件:需要使用D类(旋转器)或简化的A类指示器(进度条)。 ● 对于超过5秒的事件:建议使用动态指标A或B。

前端性能优化(四)(传输优化)
文章图片

在实际应用中,我们可以结合使用,比如:
● 先后使用D类的加载器。 ● 使用D类加载器,同时在流程中修改展示的文案。 ● 提供用户交互动画,让用户等待时忙碌起来。

除此之外,在页面加载的时候,我们还可以使用骨架屏,这里就不赘述了。
十四、防止布局改变和重新绘制 在可感知的性能领域中,更具破坏性的体验之一可能是布局转移,或者说回流,这是由重新调整图像和视频,Web字体,注入的广告或后来发现的脚本(使用实际内容填充组件)引起的。因此,当客户开始阅读文章时,可能会被阅读区域上方的布局中断。这种体验常常是突然的,而且相当令人迷惑:这可能是加载优先级需要重新考虑的情况。
社区已经开发了一些技术和解决方法来避免回流。通常,最好避免在现有内容之上插入新内容,除非它是响应用户交互而发生的。始终在图像上设置width和height属性,因此现代浏览器默认情况下会分配该框并保留空间(Firefox,Chrome)。
对于图像或视频,我们都可以使用占位符来保留媒体将出现在其中的显示框。这意味着设置并保持它的长宽比时,该区域将被适当地保留。
占位符可以是:
● 格式为png的base64字符串:低质量的小图(可以是最终图片的使用主色,或通用的颜色) ● 格式为svg的base64字符串:它比png的base64小,尤其是当宽高比率变得越来越复杂时 ● URL编码的SVG:易于阅读,易于模板化且可无限自定义,无需创建CSS块或生成base64字符串即可获得尺寸未知的图像的完美占位符!如:

data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${width} ${height}"%3E%3C/svg%3E

考虑使用本地延迟加载,而不是使用带有外部脚本的延迟加载,或者只在本地延迟加载不受支持的情况下使用混合延迟加载。
● 本地延迟加载,即:前端性能优化(四)(传输优化)
文章图片

参考资料 【前端性能优化(四)(传输优化)】Front-End Performance Checklist 2021[1]:https://www.smashingmagazine....
前端性能优化(一):准备工作[2]:https://mp.weixin.qq.com/s/QD...
前端性能优化(二):资源优化[3]:https://mp.weixin.qq.com/s/Yb...
前端性能优化(三):构建优化[4]:https://mp.weixin.qq.com/s/sp...
Hybrid Lazy Loading[5]:https://www.smashingmagazine....
Lazy-Load With IntersectionObserver[6]:https://www.smashingmagazine....
Fast load times[7]:https://web.dev/fast/#lazy-lo...
importance attribute[8]:https://developers.google.com...
BlurHash[9]:https://blurha.sh/
LQIP[10]:https://www.guypo.com/introdu...
SQIP[11]:https://github.com/axe312ger/...
polyfill[12]: https://github.com/jeremenich...
库文件[13]:https://github.com/ApoorvSaxe...
The unseen performance costs of modern CSS-in-JS libraries in React apps[14]:https://calendar.perfplanet.c...
Understanding Paint Performance with Chrome DevTools[15]:https://www.youtube.com/watch...
How to Analyze Runtime Performance[16]:https://medium.com/@marielgra...
CSS grid[17]:https://www.smashingmagazine....
Part I: Objective time management[18]:https://www.smashingmagazine....
Part II: Perception management[19]:https://www.smashingmagazine....
Part III: Tolerance management[20]:https://www.smashingmagazine....

    推荐阅读