前端性能和加载体验优化实践

2022-01-26 16:40:25 浏览数 (1)

用户为何会觉得页面卡?

1. 等待时间长(性能)

  • 项目本身包/第三方脚本比较大。
  • JavaScript 执行阻塞页面加载。
  • 图片体积大且多。

特别是对于首屏耗时中的白屏时间,用户等待的时间就越长,用户感知到页面的速度就越慢。麻省理工学院的 Richard Larson 在讲话中指出,“人类将被动等待高估了 36%”(https://mazey.cn/t/em)。这意味着用户感觉到的等待时间比开发工具记录的长得多。

2. 看起来卡(体验)

页面结构不断调整,不连贯。抖动的页面往往让用户感觉很卡。

首先通过 Webpack 插件 webpack-bundle-analyzer 分析出项目中用到的 NPM 包及大小。

性能优化

1. 构建缩包,按需加载

i. NPM
首先通过 Webpack 插件 webpack-bundle-analyzer 分析出项目中用到的 NPM 包及大小。

[点击查看大图]

结合项目可以分析出哪些包可以去除,哪些包可以有更好的替代品。

[点击查看大图]

然后在项目中移除或替换无用包,以及部分包的按需加载。

mint-ui 按需加载示例:

代码语言:javascript复制
import { Swipe, SwipeItem, Progress, Navbar, TabItem, TabContainer, TabContainerItem, Lazyload } from 'mint-ui';
Vue.use(Lazyload);Vue.component(Swipe.name, Swipe);Vue.component(SwipeItem.name, SwipeItem);Vue.component(Progress.name, Progress);Vue.component(Navbar.name, Navbar);Vue.component(TabItem.name, TabItem);Vue.component(TabContainer.name, TabContainer);Vue.component(TabContainerItem.name, TabContainerItem);

ii. 外链

不影响页面主逻辑的外链往往不是很稳定,一定要等首屏加载完成以后按需加载。

示例:

代码语言:javascript复制
// 加载其它资源if (canLoad()) {    let s = document.createElement("script");    s.onload = () => {        // ...    };    s.setAttribute(        "src",        "https://example.mazey.net/sdk.js"    );    document.body.appendChild(s);}

2.减少图片体积

i. 调整尺寸

一般来说尺寸越大,图片质量越高,则体积越大;相应的减少图片的尺寸体积会变小,但质量也会变差一些,这里就需要按照产品需求在性能和体验上寻求一个平衡。

以一个尺寸 400x400 的 GIF 图为例,尺寸转为 200x200 之后,体积由 700k 减少到 238k(-66%)。

[调整尺寸]

ii. GIF 转 WebM

GIF 作为一个存在了长达 20 年的格式,兼容性当然是最好的,但是其体积和质量对比现在流行的其他格式已经没啥优势了。目前动图常见的表现格式是 APNG、WebP。

  • APNG(AnimatedPortable Network Graphics)基于 PNG(Portable Network Graphics)格式扩展的一种动画格式,增加了对动画图像的支持,同时加入了 24 位图像和 8 位 Alpha 透明度的支持,这意味着动画将拥有更好的质量,其诞生的目的是为了替代老旧的 GIF 格式,但它目前并没有获得 PNG 组织官方的认可。APNG 被 Mozilla 社区所推崇,2008 年首次在 Mozilla Firefox 中获得支持,2017 年 Google Chrome 开始支持 APNG,截止到现在主流浏览器中只有微软家的 IE 和 Edge 不支持 APMG。
  • WebP 最初在2010年由 Google 发布,目标是减少文件大小,但达到和JPEG格式相同的图片质量,希望能够减少图片档在网络上的发送时间。WebP 有静态与动态两种模式。动态WebP(Animated WebP)支持有损与无损压缩、ICC 色彩配置、XMP 诠释数据、Alpha 透明通道。现在主流浏览器中只有 Google Chrome 和 Opera 支持 WebP。

以一个 GIF图 为例,格式转为 WebP 之后,体积由 238k 减少到 133k(-44%)。

[转图片格式]

但是 133k 的体积依旧很大,让人难以接受。作为动画效果,只要让视频循环播放,就能达到和 GIF 一样的效果,然后我又试了主流的 MP4、WebM。

[转视频]

在转成 WebM(同样是 Google 家的视频格式)之后,体积由 238k 减少到 40k(-83%)。在使用过程中加上循环播放,去除控件和加载完成后再渲染就达到了和 GIF 一样的视觉效果。

示例:

代码语言:javascript复制
<video autoplay muted name="media" loop poster="https://test.mazey.net/poster.jpg">    <source src="https://test.mazey.net/source.webm" type="video/webm"    ></video>

iii. PNG/JPG 压缩

图片上传前先通过工具压缩下(例如:https://tinypng.com/),正常都会有 50~80% 的减少。

[点击查看大图]

iv. PNG/JPG 转 WebP

PNG/JPG 转 WebP 后图片体积减少了 4-7 倍。

[转 WebP]

iv. SVG 压缩
很多矢量编辑器在导出 SVG 文件的时候,会附带很多冗余信息。

[点击查看大图]

经过 SVGO 类工具压缩之后,体积往往会缩减约 30%。

[点击查看大图]

在项目中可以使用 Webpack svgo-loader 自动压缩。

代码语言:javascript复制
module.exports = {  ...,  module: {    rules: [      {        test: /.svg$/,        use: [          {            loader: 'file-loader'          },          {            loader: 'svgo-loader',          }        ]      }    ]  }}

3.上报延迟埋点

大量业务上的埋点上报会阻塞图片加载,保证首屏渲染完成后再执行上报。

4. preconnect 预连接域名

页面中使用到的各种资源的域名较多,使用 preconnect 可以提前解析 DNS、TLS 协议、TCP 握手,节约后面加载资源时的网络请求时间。

代码语言:javascript复制
<link href="https://cdn.domain.com" rel="preconnect">
代码语言:javascript复制

5. 禁掉 favicon.ico(Webview 场景)

浏览器加载页面时,若没有指定 icon,会默认请求一个根目录下的 favicon.ico 文件,作为手机内嵌的 H5 页面,往往不需要展示图标,为了节约这个请求可以通过在 <head> 里面加上:

代码语言:javascript复制
 <link rel="icon" href="data:;base64,=">

禁掉 favicon.ico 网络请求,毕竟弱网条件下,一个网络请求相当于 500ms。

6. 启动 Gzip/Brotli 压缩

i. Gzip

Gzip 是一种用于文件压缩与解压缩的文件格式。原本是 UNIX 系统的文件压缩,后来逐渐成为 Web 最流行的数据压缩格式。它基于 Deflate 算法,可将文件无损压缩得更小,对于纯文本文件,大概可以缩减 60% 的体积,从而实现更快的网络传输,特别是对移动端非常重要。当前主流浏览器普遍地支持 Gzip,这意味着服务器可以在发送文件之前自动使用 Gzip 压缩文件,而浏览器可以在接收文件时自行解压缩文件。

[图为缩减了 64.9% 的 JavaScript 文件]

ii. Gzip

Google 认为互联网用户的时间是宝贵的,他们的时间不应该消耗在漫长的网页加载中,因此在 2015 年 9 月 Google 推出了无损压缩算法 Brotli,特别侧重于 HTTP 压缩。Brotli 通过变种的 LZ77 算法、Huffman 编码以及二阶文本建模等方式进行数据压缩,与其他压缩算法相比,它有着更高的压缩效率。针对常见的 Web 资源内容,Brotli 的性能相比 Gzip 提高了 17-25%。

除了 IE、Opera Mini 和百度浏览器,所有的主流浏览器都已经支持 Brotli。

[点击查看大图]

优化体验

1. 骨架图

页面加载中添加骨架图,骨架图根据页面基本架构生成,相对于纯白屏,体验更好。

示例:

代码语言:javascript复制
<body>    <!--骨架图-->    <svg></svg>    <!--内容-->    <div id="container"></div></body>

2.图片占位图/懒加载

图片加载的时候设置占位图,提醒用户这边会加载图片,不至于很突兀。

配合 v-lazy 实现示例:

代码语言:javascript复制
img[lazy=loading] {    background-size: contain;    background-image: url(...) ;}

懒加载示例:

代码语言:javascript复制
const imageSrc = '...';const imgLoad = new Image();imgLoad.onload = () => {    // 模拟设置图片 src    setImageSrc(imageSrc);};imgLoad.src = imageSrc;

3.3 页面防抖

首屏占位小图标直接转 Base64,必要模块设置高度,规避整个页面的抖动。

[预设一个高度,防止抖动]

其它优化

1. 引用 PWA 技术

PWA(Progressive Web App - 渐进式网页应用)是一种理念,由 Google Chrome 在 2015 年提出。PWA 它不是特指某一项技术,而是应用多项技术来改善用户体验的 Web App,其核心技术包括 Web App Manifest、Service Worker、Web Push 等,用户体验才是 PWA 的核心。

PWA 主要特点如下:

  • 可靠 - 即使在网络不稳定甚至断网的环境下,也能瞬间加载并展现。
  • 用户体验 - 快速响应,具有平滑的过渡动画及用户操作的反馈。
  • 用户黏性 - 和 Native App 一样,可以被添加到桌面,能接受离线通知,具有沉浸式的用户体验。

PWA 本身强调渐进式(Progressive),可以从两个角度来理解渐进式,首先,PWA 还在不断进化,Service Worker、Web App Manifest、Device API 等标准每年都会有不小的进步;其次,标准的设计向下兼容,并且侵入性小,开发者使用新特性代价很小,只需要在原有站点上新增,让站点的用户体验渐进式的增强。相关技术基准线:What makes a good Progressive Web App?

  • 站点需要使用 HTTPS。
  • 页面需要响应式,能够在平板和移动设备上都具有良好的浏览体验。
  • 所有的 URL 在断网的情况下有内容展现,不会展现浏览器默认页面。
  • 需要支持 Wep App Manifest,能被添加到桌面。
  • 即使在 3G 网络下,页面加载要快,可交互时间要短。
  • 在主流浏览器下都能正常展现。
  • 动画要流畅,有用户操作反馈。
  • 每个页面都有独立的 URL。

了解PWA:https://developer.mozilla.org/zh-CN/docs/Web/Progressive_web_apps。

2. 技术选型

使用 Google Workbox 构建 Service Worker
  • 什么是 Workbox ?

Workbox 是一组库,可以帮助开发者编写 Service Worker,通过 CacheStorage API 缓存资源。当一起使用 Service Worker 和 CacheStorage API 时,可以控制网站上使用的资源(HTML、CSS、JS、图像等)如何从网络或缓存中请求,甚至允许在离线时返回缓存的内容。

  • 如何使用 Workbox?

Workbox 是由许多 NPM 模块组成的。首先要从 NPM 中安装它,然后导入项目 Service Worker 所需的模块。Workbox 的主要特性之一是它的路由和缓存策略模块。

路由和缓存策略

Workbox 允许使用不同的缓存策略来管理 HTTP 请求的缓存。首先确定正在处理的请求是否符合条件,如果符合,则对其应用缓存策略。匹配是通过返回真值的回调函数进行的。缓存策略可以是 Workbox 的一种预定义策略,也可以创建自己的策略。如下是一个使用路由和缓存的基本 Service Worker。

代码语言:javascript复制
import { registerRoute } from 'workbox-routing';import {  NetworkFirst,  StaleWhileRevalidate,  CacheFirst,} from 'workbox-strategies';
// Used for filtering matches based on status code, header, or bothimport { CacheableResponsePlugin } from 'workbox-cacheable-response';// Used to limit entries in cache, remove entries after a certain period of timeimport { ExpirationPlugin } from 'workbox-expiration';
// Cache page navigations (html) with a Network First strategyregisterRoute(  // Check to see if the request is a navigation to a new page  ({ request }) => request.mode === 'navigate',  // Use a Network First caching strategy  new NetworkFirst({    // Put all cached files in a cache named 'pages'    cacheName: 'pages',    plugins: [      // Ensure that only requests that result in a 200 status are cached      new CacheableResponsePlugin({        statuses: [200],      }),    ],  }),);
// Cache CSS, JS, and Web Worker requests with a Stale While Revalidate strategyregisterRoute(  // Check to see if the request's destination is style for stylesheets, script for JavaScript, or worker for web worker  ({ request }) =>    request.destination === 'style' ||    request.destination === 'script' ||    request.destination === 'worker',  // Use a Stale While Revalidate caching strategy  new StaleWhileRevalidate({    // Put all cached files in a cache named 'assets'    cacheName: 'assets',    plugins: [      // Ensure that only requests that result in a 200 status are cached      new CacheableResponsePlugin({        statuses: [200],      }),    ],  }),);
// Cache images with a Cache First strategyregisterRoute(  // Check to see if the request's destination is style for an image  ({ request }) => request.destination === 'image',  // Use a Cache First caching strategy  new CacheFirst({    // Put all cached files in a cache named 'images'    cacheName: 'images',    plugins: [      // Ensure that only requests that result in a 200 status are cached      new CacheableResponsePlugin({        statuses: [200],      }),      // Don't cache more than 50 items, and expire them after 30 days      new ExpirationPlugin({        maxEntries: 50,        maxAgeSeconds: 60 * 60 * 24 * 30, // 30 Days      }),    ],  }),);

这个 Service Worker 使用一个网络优先的策略来缓存导航请求(用于新的 HTML 页面),当它状态码为 200 时,该策略将缓存的页面存储在一个名为 pages 的缓存中。使用 Stale While Revalidate strategy 缓存 CSS、JavaScript 和 Web Worker,将缓存的资源存储在一个名为 assets 的缓存中。采用缓存优先的策略来缓存图像,将缓存的图像存储在名为 images 的缓存中,30 天过期,并且一次只允许 50 个。

3. 客户端缓存支持

客户端在页面首次加载后把资源缓存下来,之后每次加载不进行网络请求直接读取缓存,然后再对比本次请求的版本和线上的版本,若有更新再次缓存以供下次访问,极大的缩短白屏时间。缺点是有滞后性,永远落后于线上一个版本。

4. 客户端离线包支持

为了解决客户端缓存的滞后问题,离线包方式是一种提前下载页面资源的方式。缺点是占用用户更多的流量,优点是能够实现真正意义上的页面“秒开”。

[点击查看大图]

4. 优化后端接口数据

首屏动态渲染受制于后端接口返回的数据,如果接口存在体积大、有前后依赖关系、数量多需要耦合等问题,首屏渲染因为等待数据往往会比较慢。解决办法是拉上后端一起梳理下哪些数据才是首屏所需要的,用一个接口把首屏数据输送给前端。

5. 优化占用内存

在浏览器控制台的 Performance 栏位,可以记录整个页面生命周期的每一个细节,其中有大量描述 JavaScript 堆栈内存占用的情况。

[点击查看大图]

CPU 内存

CPU memory is attached to the CPU, and is almost universally two DIMMs wide (128b), and is a multi-drop bus (so requires more power and conditioning to drive, even at lower clocks.) Of course, we generally expect to be able to configure CPU memory by snapping in different DIMMs, so the CPU’s memory controller is far more complicated and flexible.

JavaScript 对内存的占用受代码的影响,如果在运行时缓存和计算大量的数据、处理巨量字符串等耗费空间的行为,那么内存就会极速飙升,极端情况下会导致承载网页的应用闪退。

GPU 显存

GPU memory is attached to the GPU, and is a wider interface, with shorter paths and a point-to-point connection. As a consequence, it generally runs at higher speed (clock) than CPU memory. It’s common for GPU memory to deliver several hundred GB/s to the GPU; for a CPU, it’s in the mid tens of GB/s. (There are higher-end CPUs with very wide interfaces that are around 100 GB/s.) The internal design of both kinds of memory is very similar.

[点击查看大图]

经测试,这部分内存受屏幕尺寸和帧数影响较大,如果是动画或高精度的图片渲染时,则内存会向上浮动。

6. 预渲染

动态渲染的页面,首屏需要等待 JavaScript 加载完成之后才能执行渲染,等待 JavaScript 加载的时间越久,白屏的时间越久。而通过在 CI/CD 阶段,将传统 SSR 的流程执行一遍,用动态生成的 index.html 覆盖原来“空的”index.html,即优化了首屏耗时体验,省去了骨架屏的步骤,也提升了加载速度。使用 prerender-spa-plugin 可以轻松配置预渲染页面,现已经被 React/Vue 项目广泛应用。

上述首屏耗时优化效果最终评估平台为:腾讯云前端性能监控。点击文末「阅读原文」了解腾讯云前端性能监控。

联系我们

扫码加云监控小助手,回复“Rum”

加入前端性能监控技术交流群

RUM 相关文章:


关注我们,了解腾讯云监控的最新动态

本文转载于腾讯云 社区,原文链接:

https://cloud.tencent.com/developer/article/1919102。

0 人点赞