5
头图
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。
image.png

二、使用 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]。

<img data-src="lazy.jpg" loading="lazy" alt="Lazy image">
<script>
  (function() {
    const images = document.querySelectorAll("[loading=lazy]");
    if ("loading" in HTMLImageElement.prototype) {
      images.forEach(function(one) {
        one.setAttribute(
          "src",
          one.getAttribute("data-src")
        );
      });
    } else {
      const config = { … };

      let observer = new IntersectionObserver(function (entries, self) {
        entries.forEach(entry => {
          if (entry.isIntersecting) { … }
        });
      }, config);
      images.forEach(image => { observer.observe(image); });
    }
  })();
</script>

想了解更多关于懒加载的可以阅读Google的Fast load times[7]。

另外,我们可以在DOM节点上使用important属性[8],重置资源的优先级。它可以用<script>, <img>, <link>标签上,还可以用于fetch请求上。它接受以下三个值:

● high:如果浏览器的启发式不阻止,则可以优先考虑资源。
● low:如果浏览器的启发式允许,则可以降低资源的优先级。
● auto:默认值,让浏览器决定哪种优先级适用于资源。

根据你的网络堆栈,“Priority Hints”的影响会稍有不同。使用HTTP/1.X,浏览器确定资源优先级的唯一方法是延迟请求的发出时间。结果,假设队列中有较高优先级的请求,则较低优先级的请求仅在高优先级的请求之后进入网络。如果没有,浏览器如果预测很快就会出现更高优先级的请求(例如,如果文档的<head>仍处于打开状态并且可能在其中发现关键资源),浏览器可能仍会延迟一些低优先级的请求。

使用HTTP/2,浏览器可能仍会延迟一些低优先级的请求,但除此之外,它还可以将其资源的流优先级设置为较低的级别,从而使服务器能够更好地确定其向下发送的资源的优先级。

如何查看资源加载的优先级,在Chrome的DevTools,打开Network面板 -> 右击 -> 勾上Priority即可,如下图:
image.png

此时,你就可以看到你的资源加载优先级了。
image.png

三、渐进式加载图片

可以先加载低质量甚至模糊的图片,然后随着页面继续加载,通过使用BlurHash技术或LQIP(低质量图像占位符)技术将其替换为完整质量版本。是否改善了用户体验,众说不一,但它确实提高了首次进行有意义绘制的时间。我们甚至可以通过使用 SQIP创建一个低质量的图片版本作为 SVG 占位符,或者使用 CSS 线性渐变(可以使用cssgip)作为渐变图片占位符来自动实现。

● BlurHash[9],一个网站,支持将上传的图片转为模糊图片
● LQIP[10],初始加载页面时使用低质量图片,一旦页面加载完成后使用高质量图片替换。
● SQIP[11],帮助创建低质量版本的图像作为SVG占位符。

如何实现呢?我们可以使用Intersection Observer API。当然,如果浏览器不支持intersection observer,我们可以加上polyfill[12]或某些库文件[13]继续使用它。‍

// https://calendar.perfplanet.com/2017/progressive-image-loading-using-intersection-observer-and-sqip/
<img class="js-lazy-image" src="dog.svg" data-src="dog.jpg">
// Get all of the images that are marked up to lazy load
const images = document.querySelectorAll('.js-lazy-image');
const config = {
  // If the image gets within 50px in the Y axis, start the download.
  rootMargin: '50px 0px',
  threshold: 0.01
};

// The observer for the images on the page
let observer = new IntersectionObserver(onIntersection, config);
images.forEach(image => {
  observer.observe(image);
});

function onIntersection(entries) {
  // Loop through the entries
  entries.forEach(entry => {
    // Are we in viewport?
    if (entry.intersectionRatio > 0) {
      // Stop watching and load the image
      observer.unobserve(entry.target);
      preloadImage(entry.target);
    }
  });
}

四、以内容可见性推迟渲染

使用content-visibility:auto,当容器位于视口之外时,我们可以提示浏览器跳过子级的布局。

footer {
  /* Only render when in viewport */
  content-visibility: auto;
  contain-intrinsic-size: 1000px;
  /* 1000px is an estimated height for sections that are not rendered yet. */
}

注意,content-visibility: auto,表现与overflow: hidden一致。可以使用padding-left、padding-right和声明的width修复。padding基本上允许元素溢出内容框并进入填充框,而不必离开整个框模型并切断它们。

body > .row {
  padding-left: calc((100% - var(--contentWidth)) / 2);
  padding-right: calc((100% - var(--contentWidth)) / 2);
}

另外,css的contain属性也值得了解一下。它允许开发者声明当前元素和它的内容尽可能的独立于 DOM 树的其他部分。这使得浏览器在重新计算布局、样式、绘图、大小或这四项的组合时,只影响到有限的 DOM 区域,而不是整个页面,可以有效改善性能。它有如下取值:

● layout:表示元素外部无法影响元素内部的布局,反之亦然。这允许浏览器潜在地减少创建页面布局时所需的计算数量,另一个好处是,如果所包含的元素不在屏幕上或以某种方式被遮盖,则浏览器可能会将相关计算延迟或转移到较低的优先级。
它有一个问题:虽然其子元素不会影响也页面的元素,但子元素会影响当前元素,如果子元素增加或减少,当前元素的size也会跟着受影响,从而影响页面上的其他元素。
● paint:通知浏览器,该元素的任何后代都不会被绘制在该元素的边框之外。如果将后代元素定位为使其边界框的一部分被所包含元素的边框所裁剪,则该部分将不被绘制。如果后代元素完全位于所包含元素的边框的外部,那么它根本不会被绘制。这与overflow:hidden很像,但overflow:hidden没有减少或跳过所需要技术的好处。
● size:通知浏览器在执行页面布局计算时无需考虑任何后代。所包含的元素必须应用了height和width属性,否则它将折叠为零像素正方形。页面布局计算只需要考虑元素本身,因为后代不能影响元素的大小。在这种计算中,所包含元素的后代将被完全跳过;好像它根本没有后代。
● style:表示那些同时会影响这个元素和其子孙元素的属性,都在这个元素的包含范围内。
● content:layout与paint的结合。
● strict:layout、paint、size的结合。

五、使用decoding="async"推迟解码

使用decoding="async"授予浏览器不在主线程解码图像的权限,避免了用户对用于解码图像的CPU时间的影响。

<img decoding="async" … />

对于屏幕外图像,我们可以先显示一个占位符,然后当图像在视口中时,使用IntersectionObserver触发网络调用,以将图像下载到后台。另外,我们可以推迟渲染,直到使用img.decode()进行解码为止;如果Image Decode API不可用,则可以下载图像。

// Image loading with predecoding
// -------------------------------------
const img = new Image();
img.src = "bigImage.jpg";
img.decode().then(() => {
    document.body.appendChild(img);
}).catch(() => {
    throw new Error('Could not load/decode big image.');
});

六、生成并提供关键CSS

关键CSS即页面第一次可见部分的CSS(或称首屏显示的CSS),通常内联到html的<head>中。由于缓存,将关键的CSS(和其他重要资源)放在根域的单独文件中有时甚至比内联有很多好处。注意:使用HTTP / 2时,关键CSS可以存储在单独的CSS文件中,并且可以通过服务器推送来传递,而不会膨胀HTML。问题在于,服务器推送存在许多浏览器之间的陷阱和竞争条件,因此很麻烦。

我们可以使用criticalCSS和critical生成关键CSS,使用webpack的插件critters实现内联关键css,懒加载剩下的。

如果你还在使用loadCss异步加载你的所有css,那就没必要了。使用media="print"可以欺骗浏览器异步加载CSS,只要加载完成,就立即应用在屏幕环境。

<!-- Via Scott Jehl. https://www.filamentgroup.com/lab/load-css-simpler/ -->
<!-- Load CSS asynchronously, with low priority -->
<link rel="stylesheet"
  href="full.css"
  media="print"
  onload="this.media='all'" />

七、尝试重新组合你的CSS规则

CSS对性能如此关键的原因如下:

● 浏览器在构建了“渲染树”之前无法渲染页面;
● 渲染树是DOM和CSSOM的组合结果;
● DOM是HTML加上需要对其执行操作的所有阻止JavaScript;
● CSSOM是应用于DOM的所有CSS规则;
● 使用async和defer属性可以很容易地使JavaScript不受阻碍;
● 使CSS异步要困难得多;
● 因此要记住的一个很好的经验法则是,页面的渲染速度仅与最慢的样式表一样快。

如果我们可以将单个完整的CSS文件拆分为各自的媒体查询,如下:

<link rel="stylesheet" href="all.css" media="all" />
<link rel="stylesheet" href="small.css" media="(min-width: 20em)" />
<link rel="stylesheet" href="print.css" media="print" />

浏览器会下载所有的文件,但只有满足当前上下文所需的文件才会阻塞渲染。

另外,在CSS文件中要避免使用@import,因为它需要等待当前的CSS文件下载完成之后才去下载。

css link会阻塞页面解析和渲染,当我们在link后面放置一段JS片段,它需要等待直到CSS下载完成并解析完成(即CSSOM生成)。同样的,它也会阻塞异步的JS加载。最好的方式就是将不依赖CSS的JS放置于它前面。

内联关键CSS就没法利用浏览器缓存,我们可以使用service worker解决这个问题:一般而言,为了使用 JavaScript 快速查找到 CSS,我们需要添加一个 ID 属性到 style 元素上,然后 JavaScript 可以使用缓存 API 来将其存储在本地浏览器缓存(内容格式为 text/css)中,以用于随后的页面。为了避免在后续页面上进行内联,从外部引用缓存的资源,我们在第一次访问一个站点时设置了一个 cookie。

// https://www.filamentgroup.com/lab/inlining-cache.html
<style id="css">
.header { background: #09878}
h1 { font-size: 1.2em; col… }
h2 { margin: 0; }
…
</style>
<script>
if( "caches" in window ){
  var css = document.getElementById("css").innerHTML;
  if( caches ){
    caches.open('static').then(function(cache) {
      cache.put("site.css", new Response( css,
        {headers: {'Content-Type': 'text/css'}}
      ));
    });
  }
}
</script>

值得注意的是,动态样式也可能很昂贵,但通常仅在您依赖于数百个同时渲染的合成组件的情况下。因此,如果使用的是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,<link rel =“ preload”> vs <link rel =“ prefetch”>,更多关于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。Service  Worker 通过响应 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 的页面需要再次加载才会受其控制,线程实施控制后,它将处于以下两种状态之一: 服务工作线程终止以节省内存,或处理获取和消息事件,从页面发出网络请求或消息后将会出现后一种状态。

image.png

(三)先决条件

● 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属性添加到<img>标签来将跨域图像资源选择为进入CORS模式。

(八)safari's range request

Safari发送初始请求以获取视频,并将Range标头设置为bytes = 0-1。你可以看到,Safari需要提供视频和音频的HTTP服务器来支持这样的Range请求。而Service Worker是有问题的。要解决此问题,可以对Service Worker进行如下设置:

// https://philna.sh/blog/2018/10/23/service-workers-beware-safaris-range-request/
self.addEventListener('fetch', function(event) {
  var url = new URL(event.request.url);
  if (url.pathname.match(/^\/((assets|images)\/|manifest.json$)/)) {
    if (event.request.headers.get('range')) {
      event.respondWith(returnRangeRequest(event.request, staticCacheName));
    } else {
      event.respondWith(returnFromCacheOrFetch(event.request, staticCacheName));
    }
  }
  // other strategies
});
// Range Header:Range: bytes=200-1000
function returnRangeRequest(request, cacheName) {
  return caches
    .open(cacheName)
    .then(function(cache) {
      return cache.match(request.url);
    })
    .then(function(res) {
      if (!res) {
        return fetch(request)
          .then(res => {
            const clonedRes = res.clone();
            return caches
              .open(cacheName)
              .then(cache => cache.put(request, clonedRes))
              .then(() => res);
          })
          .then(res => {
            return res.arrayBuffer();
          });
      }
      return res.arrayBuffer();
    })
    .then(arrayBuffer => {
      // 拿到array Buffer,处理Range Header
      const bytes = /^bytes\=(\d+)\-(\d+)?$/g.exec(
        request.headers.get('range')
      );
      if (bytes) {
        const start = Number(bytes[1]);
        const end = Number(bytes[2]) || arrayBuffer.byteLength - 1;
        return new Response(arrayBuffer.slice(start, end + 1), {
          status: 206,
          statusText: 'Partial Content',
          headers: [
            ['Content-Range', `bytes ${start}-${end}/${arrayBuffer.byteLength}`]
          ]
        });
      } else {
        return new Response(null, {
          status: 416,
          statusText: 'Range Not Satisfiable',
          headers: [['Content-Range', `*/${arrayBuffer.byteLength}`]]
        });
      }
    });
}

十二、优化渲染性能

确保在滚动页面或元素展示动画效果时没有延迟,能始终达到每秒 60 帧;如果达不到,至少也要使每秒帧数在 60 到 15 的混合范围内。你可以使用css will-change通知浏览器哪些元素和属性将会改变。

在不改变DOM和它的样式的前提下,会触发重绘的有:GIF、canvas绘制、animation。为避免重绘,我们需尽可能的使用opacity、transform,除非在有一些特殊情况,如为SVG路径设置动画时才会触发重绘。我们可以检测没必要的重绘DevTools → More tools → Rendering → Paint Flashing

除了Paint Flashing,还有多个比较有趣的工具选项,比如:

● Layer borders:用于显示由浏览器渲染的图层边框,以便可以轻松识别大小的任何变换或更改。
● FPS Meter:实时显示浏览器当前帧数。
● Paint flashing:用于突出显示浏览器被迫重绘的网页区域。Umar Hansa关于Understanding Paint Performance with Chrome DevTools[15]的视频值得看一看。

文章How to Analyze Runtime Performance[16]详细介绍了如何分析运行时的性能,非常有用,对Chrome DevTools的Performance还不太会用的同学,推荐阅读。

(一)如何测量样式和布局计算花费的时间?

requestAnimationFrame可以作为我们的工具,但它有一个问题,什么时候执行回调函数不同的浏览器表现不一样:Chrome、FF、Edge >= 18在样式和布局计算之前触发,Safari、IE、Edge < 18在样式和布局计算之后绘制之前触发。

如果在requestAnimationFrame的回调中调用setTimeout,在符合规范的浏览器中(如Chrome),setTimeout的回调函数将在绘制之后调用;在不符合规范的浏览器中(如Edge 17),requestAnimationFrame和setTimeout几乎同时出发,均在样式和布局计算完成之后触发。

如果在requestAnimationFrame的回调中调用microtask,如Promise.resolve,这完全没用,JavaScript执行完成后立即运行,因此,它根本不会等待样式和布局。

如果在requestAnimationFrame的回调中调用requestIdleCallback,它会在绘制完成之后触发。但是,这个很能会触发得太晚。它启动速度相当快,但如果主线程忙于执行其他工作,则requestIdleCallback可能会延迟很长时间,以等待浏览器确定运行某些“空闲”工作是安全的。这肯定比setTimeout少得多。

如果在requestAnimationFrame的回调中调用requestAnimationFrame,它会在绘制完成之后触发,与setTimeout相比,它可能会捕获更多的等待时间。在60Hz屏幕上大约需要16.7毫秒,而setTimeout的标准时间是4毫秒–因此稍微不准确。

总的来说,requestAnimationFrame + setTimeout尽管有缺陷,但可能仍然比requestAnimationFrame + requestAnimationFrame更好

(二)关于瀑布流布局你了解吗?

仅使用CSS grid[17]马上就可以支持。有兴趣的可以多了解一下,这里就不叙述了。‍

.container {
  display: grid;
  // 创建一个四列布局
  grid-template-columns: repeat(4, 1fr);
  grid-template-rows: masonry;
}

(三)CSS动画

当我们写CSS动画时,我们通常被告知如果实现动画,使用transform实现。比如修改元素的位置,为什么不直接用我们常用的left、top呢?因为浏览器需要不断计算元素的位置,这会触发回流;又比如修改图片在页面的显示状态,会触发重绘。重绘的代价通常都是非常高的。

如果想要动画看起来流畅,需要注意三点:

● 不要影响文档的流程
● 不依赖于文档的流程
● 不会导致重绘

符合以上条件的有:

● 3D transforms: translate3d, translateZ等等;
● <video>, <canvas>, <iframe>元素;
● 使用Element.animate()进行的transform、opacity动画 ;
● 使用СSS transitions和animations进行的transform、opacity动画 ;
● position: fixed;
● will-change;
● filter;

浏览器使用这些不会导致回流或重绘时,就可以应用合成优化:将元素绘制到单独的图层然后并将其发送到GPU合成。这就是我们常说的脱离文档流。需注意,如果某个元素脱离了文档流,那显示在其上面的元素也都会被隐式的脱离文档流。

单个合成层需要多少内存?我们举一个简单的例子:存储320×240像素的矩形(用纯色#FF0000颜色填充)需要多少内存。问题在于,PNG与JPEG,GIF等一起用于存储和传输图像数据。为了将这样的图像绘制到屏幕上,计算机必须将其从图像格式中解压缩,然后将其表示为像素阵列。因此,我们的示例图像将占用320×240×3 = 230,400字节的计算机内存。也就是说,我们将图片的宽度乘以图片的高度即可得出图片中的像素数。然后,我们将其乘以3,因为每个像素都由三个字节(RGB)描述。如果图片包含透明区域,则将其乘以4,因为需要额外的字节来描述透明度:(RGBa):320×240×4 = 307,200字节。浏览器始终将脱离文档流的图层绘制为RGBa图像。从技术上来讲,可以在GPU中存储PNG图像以减少内存占用,但GPU有个问题,它是逐像素绘制的,这意味着它必须为整个PNG图像一次又一次地解码每个像素。

如果你想查看你的网站有多少个图层以及这些图层消耗了多少内存,在Chrome,进入chrome://flags/#enable-devtools-experiments启动"Developer Tools experiments"标识,然后使用⌘+⌥+ I(在Mac上)或Ctrl + Shift + I(在PC上)打开DevTools,单击右上角的摧垂直的三个点的图标,选择"More Tools",点击"Layers"即可在DevTools看到该面板。此面板将当前页面的所有活动图层显示为树,选取图层时,你会看到诸如其大小,内存消耗,重绘次数和合成原因之类的信息。

浏览器是如何处理动画的?

这里我们以点击触发动画的形式为例。

● 首先,页面加载后,浏览器没有理由将元素放置在新的图层,因此一开始的时候元素停留在默认的文档流里。
● 当我们点击按钮时,会将元素放置在新的图层层。将元素提升到新的图层会触发重绘:浏览器必须为元素在新的图层绘制纹理,同时删除背景层(默认文档流)里的。
● 必须将新的图层图像传输到GPU,以实现用户在屏幕上看到的最终图像合成。根据层数,纹理的大小和内容的复杂性,重绘和数据传输的执行时间,可能会花费大量时间。这就是为什么我们有时会在动画开始或结束时看到元素闪烁的原因。
● 动画结束后,我们就有了删除该新图层的理由了,再一次,浏览器发现不需要在合成上浪费资源,因此它又回到了最佳策略:将页面的全部内容保留在单个图层上,这意味着它必须在背景层上绘制元素(另一次重绘)并将更新后的纹理发送到GPU。与上述步骤一样,这可能会导致闪烁。

为了消除隐式产生的新图层问题并减少视觉伪像,建议以下操作:

● 尝试使动画对象在z-index尽可能高。理想情况下,这些元素应该是body元素的直接子元素。当然,由于正常布局使得动画元素嵌套在DOM树的深处时,并不总是能够做到这一点。在这种情况下,你可以克隆元素并将其放在主体中仅用于动画。
● 使用will-change CSS属性你可以向浏览器提示此元素将脱离普通文档流,通过在元素上设置此属性,浏览器将(但并非总是如此!)提前将其提升到新的图层,以便动画可以平稳地启动和停止。但是请不要滥用此属性,否则最终会导致内存消耗大量增加!

针对CSS动画,我们可以做如下优化:

● 仅使用transform和opacity做动画,它们不会触发回流与重绘。
● 如果是纯色图片,减少图片的物理尺寸大小,通过将图片放大的方式以达到效果,这有助于减少内存的使用。如果你想给比较大的图片的加上动画,通常你可以将此图片缩小5%到10%,然后再放大,用户可能看不到任何区别,这样你就可以节省甚至几兆的宝贵内存。
● 尽可能的使用css 的transition或transform,与JS相比,它更快,且不会被繁重的JS计算阻塞。

(四)渲染性能优化清单

● font-display:加快显示自定义字体。默认情况下,使用自定义字体的所有文本在加载这些字体(或不超过3秒)之前都是不可见的。 
● 将web字体自托管:自托管(如果使用Google字体,可以使用google-fonts-webpack-plugin),同时使用字体子集,有助于更快地加载字体。
● /*#__PURE__*/:如果你有一个函数,仅调用一次,然后将结果存储在变量中,但不使用该变量,添加此段代码放在函数调用前面,tree-shaking将删除该变量,但不删除该函数。
● 使用babel-plugin-styled-components和styled-components:这些插件在CSS-in-JS声明的前面加/*#__ PURE __ */。没有它们,未使用的CSS规则将不会从捆绑包中删除。
● 检测没必要的重绘:DevTools → More tools → Rendering → Paint Flashing。
● 检测code splitting是否作用良好:DevTools → Ctrl/⌘+P → Coverage。
● 检测资源是否未使用gzip/Brolti压缩:在"Network"面板的filter中输入"-has-response-header:Content-Encoding",可以找到所有未使用gzip/Brolti压缩的资源。
● 检测第三方资源/性能分析js是否影响你的网站性能:Network → Sort by domain → Right-click each third-party → Select "Block request domain";重新执行对比。
● preload网络字体时添加crossorigin="anonymous"属性:由于CORS欺骗,如果没有该属性,则将忽略预加载的字体。
● image-webpack-loader:将此loader插入url-loader或file-loader的前面,它将根据需要压缩和优化图像。
● responsive-loader:结合<img srcset> 使用,在较小的屏幕上投放较小的图片。
● svg-url-loader:如果使用url-loader加载svg,由于字母的限制,base64编码的资源平均比原始资产大37%。
● purgecss-webpack-plugin:删除未使用的类,即删减未使用的css。
● babel-plugin-lodash:转换Lodash导入,以确保仅绑定了实际使用的方法(= 10-20个函数,而不是300个);另外尝试给lodash添加别名lodash-es(反之亦然),以避免使用不同Lodash版本依赖关系不同,到此多次打包。
● 如果你使用babel-preset-env和Core.js 3+,启用'useBuiltIns': "usage":这将只打包你实际使用和需要的polyfill。
● 如果使用HTMLWebpackPlugin,启动`optimization.splitChunks: 'all'`:使webpack自动对入口文件进行代码拆分,以实现更好的缓存。
● 设置`optimization.runtimeChunk: true`:这样可以将webpack的运行时移到一个单独的块中,也可以改善缓存。
● webpack-bundle-analyzer:帮忙分析打包文件,避免文件过大。
● 使用http://webpack.github.io/analyse/:弄清楚为什么打包的包中包含特定模块。
● preload-webpack-plugin:与HTMLWebpackPlugin一起使用,并为所有JS块生成<link rel =“ preload / prefetch”>。
● duplicate-package-checker-webpack-plugin:如果打包同一库的多个版本(对于core-js来说是超级常见的),则会发出警告。
● bundle-buddy:显示哪些模块在你的块中重复,用它来微调代码拆分。
● source-map-explorer:根据source map构建模块和依赖关系的映射。与webpack-bundle-analyzer不同,它只需要运行source map即可。如果你无法编辑webpack配置(例如,使用create-react-app),则很有用。
● bundle-wizard:也建立了一个依赖关系图–但是对于整个页面。
● Day.js:替换Moment.js,它有同样的功能,且更小。
● linaria:替代styled-components或emotion,具有类似API的0运行时替代方案。
● quicklink:旨在为网站根据用户视口中的内容prefetch链接的嵌入式解决方案,且体积小(缩小/压缩后<1KB)。
● Service Worker
● HTTP/2:要检查所有请求是使用单个HTTP/2连接还是配置有误,可在DevTools→Network中启用“Connection ID”列。
● 使用Cache-Control: immutable:缓存静态文件
For API responses (like /api/user): prevent caching
→ Cache-Control: max-age=0, no-store
For hashed assets (like /static/bundle-ab3f67.js): cache for as long as possible
→ Cache-Control: max-age=31556952, immutable
● 将CSS分为两部分:关键CSS、屏幕之下的CSS
● defer第三方资源或使用setTimeout包裹加载:避免第三方脚本会争夺带宽和CPU时间。
● 如果你有任何`scroll`或`touch*`事件, 确定传递`passive: true`给addEventListener:这告诉浏览器你不打算在内部调用event.preventDefault(),因此可以优化处理这些事件的方式。
● 不要交叉获取或设置样式属性"width"或"offset*":每次更改然后读取宽度或其他内容时,浏览器都必须重新计算布局。
● 使用http://polyfill.io减少使用的Polyfill数量:检查User-Agent标头,并提供专门针对浏览器的polyfill。因此,现代的Chrome用户无需加载Polyfill,而IE 11用户则获取所有。
● 工具:Lighthouse CLI、WebPageTest

十三、优化感知性能18[20]

该概念涉及等待的心理,基本上就是使用户忙碌或参与其他事情。

从时间上来说,可以从两个不同的观点进行分析:客观时间也叫时钟时间(即真正花费的时间);感知时间也叫大脑时间(即用户感知的时间)。

时间就是金钱,在哪儿都适用。2015年调查发现,访客仅需3秒钟即可放弃网站。每提高1秒钟,沃尔玛在其网站上的转化率就提高了2%。当汽配零售商AutoAnything将加载时间减少一半时,其销售额增长了13%。

我们将时间分为4个跨度:

● 0.1-0.2s:研究指出此时间间隔是模拟瞬时行为的最大可接受响应时间范围,在该时间间隔内,用户几乎(如果有的话)几乎不会注意到延迟。
● 0.5-1s:这是即时行为的最大响应时间。该时间间隔通常是对话者在人与人之间的对话中的响应时间。在此时间间隔内的延迟很明显,但大多数用户很容易容忍。在这段时间内,必须给用户一个指示,已经收到用户的交互行为,比如点击。
● 2-5s:最佳体验时间根据主观参数而有所波动,但对于普通用户在网络上面临的大多数任务,专心的时间介于2到5秒之间。这就是为什么多年来我们以2秒作为最佳页面加载时间的原因。
● 5-10s:根据美国国家医学图书馆国家生物技术信息中心的数据,人的平均注意力跨度从2000年的12秒下降到2015年的8.25秒。猜猜是什么?比金鱼的注意力时间少1秒!为简化起见,并强调我们在金鱼上的优越性,我们将10秒视为用户注意力跨度的绝对最大时间。用户仍然会专注于自己的任务,但很容易分心。这是系统让用户参与该过程的时间。如果不这样做,那么用户很可能会永远丢失。

image.png

因此,页面加载应即时发生,用户应从给定的操作中获得立即反馈。

有一点需要记住: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的问题,就是我们要说的容忍管理。首先,解决等待时间和进程状态的不确定性,我们可以使用良好的进度指示。进度指示又分为两类:动态(随着时间更新)、静态(一直不变的),其中每一个都可以再分为两个子类:确定的(按工作单位,时间或其他度量方式完成的项目)、不确定的(不完成项目)。
image.png

如何选择呢?我们可以按之前的时间跨度去区分:

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

image.png

总的来说就是:

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

image.png

在实际应用中,我们可以结合使用,比如:

● 先后使用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

考虑使用本地延迟加载,而不是使用带有外部脚本的延迟加载,或者只在本地延迟加载不受支持的情况下使用混合延迟加载。

● 本地延迟加载,即:<img loading=lazy>(虽然大部分浏览器已经支持了,但还是存在兼容问题)。
● 外部脚本延迟加载,即延迟加载屏幕之下的图片,可以使用Intersection Observer API,或使用scroll, resize, or orientationchange事件监听。
● 混合延迟加载,即在不支持本地延迟加载的时候,使用脚本延迟加载。

混合延迟加载

首先检测浏览器是否支持本地延迟加载:

if ('loading' in HTMLImageElement.prototype)

简单点,之前我们已经介绍过使用Intersection Observer,这里就不多说了。

始终要将 web 字体重绘分组,一次性从所有降级字体转换为 web 字体—只需确保这种转换不会太突然,通过使用字体样式匹配器调整字体之间的行高和间距即可。在之前我们介绍字体优化时已经讲解,这里就不多说了。

要覆盖后备字体以模拟网络字体的字体指标,我们可以使用@font-face描述符覆盖字体指标(已在Chrome 87中启用)。(请注意,尽管如此,调整也会因复杂的字体堆栈而变得复杂。)提议的描述符有:

● ascent-override, descent-override, line-gap-override

建议语法:<percentage> | normal
初始值:normal
状态:已在M86中实施;同意在CSSWG运送

这些描述符使我们可以完全消除垂直布局偏移(除非由于换行不同而导致行数不同。),在计算行高时,将ascent,descent或line gap设置为所用字体大小的给定百分比。这使我们可以覆盖线框的高度(line box height)和基线位置(baseline positioning):

线框高度 = ascent + descent + line gap
基线位置 = line box top + line gap / 2 + ascent

例如,如果我们有'overcent-override:80%; descent-override:20%;line-gap-override:0%”,则每个线框的高度为1em(假设使用的字体为1em),基线位于线框顶部下方0.8em。

● advance-override

建议语法:<number>
初始值:0

此描述符使我们可以减少水平换行以及由不同换行引起的垂直换行。

该描述符为使用字体的每个字符设置了一个额外的提前量。超前量等于描述符值乘以使用的字体大小。

注意:除了CSS属性letter-spacing外,还可以应用此属性。例如,如果我们有'font-size:20px; letter-spacing:-1px,和font face的“advance-override:0.1”,则最终字符之间的间距为20px * 0.1 - 1px = 1px。

对于较晚的CSS,我们可以确保在每个模板的标头中都内嵌关键布局的CSS。甚至还不止于此:对于长页面,添加垂直滚动条后,确实会将主要内容向左移动16px。要尽早显示滚动条,我们可以添加overflow-y到html上以强制在第一次绘制时使用滚动条。后者之所以有用,是因为当宽度变化时,由于折叠内容的重排,滚动条会导致不平凡的布局移位。不过,大多数情况下应该发生在具有非重叠滚动条的平台上,例如Windows。但是这会破坏position:sticky,因为这些元素永远不会滚动出容器(position:sticky只会在父元素的overflow等于visible时才会生效)。

如果标题在页面滚动的过程中变成fixed或sticky,则在此之前要为标题保留空间,因为从布局流中删除标题并将其粘贴到页面顶部,所有后续内容都将上移。

如果列表后面还有内容,无限滚动和加载更多也会触发CLS。为了改善CLS,请在用户滚动到页面的该部分之前为要加载的内容保留足够的空间,并删除页脚或页面底部的任何DOM元素,这些元素可能会由于内容加载而被下推。预取未折叠内容的数据和图像,以便在用户滚动到该位置时就已经存在。还可以使用虚拟列表来优化长列表。

如何计算CLS,我们在之前已经讲过,这里就不说明了。

检测什么导致的布局偏移:Chrome DevTools > Performance panel > Experience

如何统计CLS呢?可以参考下面这段代码:

// 原文:https://wicg.github.io/layout-instability/
let perFrameLayoutShiftData = [];
let cumulativeLayoutShiftScore = 0;

function updateCLS(entries) {
  for (const entry of entries) {
    // Only count layout shifts without recent user input.
    if (entry.hadRecentInput)
      return;

    perFrameLayoutShiftData.push({
      score: entry.value,
      timestamp: entry.startTime
    });
    cumulativeLayoutShiftScore += entry.value;
  }
}

// Observe all layout shift occurrences.
const observer = new PerformanceObserver((list) => {
  updateCLS(list.getEntries());
});
observer.observe({type: 'layout-shift', buffered: true});

// Send final data to an analytics back end once the page is hidden.
document.addEventListener('visibilitychange', () => {
  if (document.visibilityState === 'hidden') {
    // Force any pending records to be dispatched.
    updateCLS(observer.takeRecords());

    // Send data to your analytics back end (assumes `sendToAnalytics` is
    // defined elsewhere).
    sendToAnalytics({perFrameLayoutShiftData, cumulativeLayoutShiftScore});
  }
});

欢迎关注我的个人公众号:
image.png

参考资料

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


花伊浓
55 声望2 粉丝