关于前端:前端性能优化四传输优化

70次阅读

共计 30439 个字符,预计需要花费 77 分钟才能阅读完成。

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]。

<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即可,如下图:

此时,你就能够看到你的资源加载优先级了。

三、渐进式加载图片

能够先加载低质量甚至含糊的图片,而后随着页面持续加载,通过应用 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 的页面须要再次加载才会受其管制,线程施行管制后,它将处于以下两种状态之一: 服务工作线程终止以节俭内存,或解决获取和音讯事件,从页面收回网络申请或音讯后将会呈现后一种状态。

(三)先决条件

● 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 秒视为用户注意力跨度的相对最大工夫。用户依然会专一于本人的工作,但很容易分心。这是零碎让用户参加该过程的工夫。如果不这样做,那么用户很可能会永远失落。

因而,页面加载应即时产生,用户应从给定的操作中取得立刻反馈。

有一点须要记住: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

思考应用本地提早加载,而不是应用带有内部脚本的提早加载,或者只在本地提早加载不受反对的状况下应用混合提早加载。

● 本地提早加载,即:<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});
  }
});

欢送关注我的集体公众号:

参考资料

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

正文完
 0