【腾讯云前端性能优化大赛】前端性能和加载体验优化实践(附:PWA、离线包、内存优化、预渲染)

访问地址:https://bbs.mihoyo.com/bh3/

PWA:仅支持在 IOS 端添加到桌面。

A.2.2 阿里速卖通(AliExpress)

访问地址:https://m.aliexpress.com/

PWA:使用 Google Workbox(CDN)

  1. 支持添加到桌面,manifest
  2. 支持缓存,Service Worker

A.2.3 饿了么

访问地址:https://h5.ele.me/msite/

PWA:自研 - PWA 在饿了么的实践经验

  1. 支持添加到桌面,manifest
  2. 支持缓存和离线访问,Service Worker

A.2.4 Instagram

左边原生应用,右边 PWA

访问地址:https://www.instagram.com/

PWA:使用 Google Workbox

  1. 支持添加到桌面,manifest
  2. 支持缓存,Service Worker

A.2.5 Twitter

访问地址:https://mobile.twitter.com/home

PWA:Twitter 自研 - How we built Twitter Lite

  1. 支持添加到桌面,manifest
  2. 支持缓存和离线访问,Service Worker

除了正常的静态资源以外,Twitter 把首页也缓存了下来。

离线状态下有很好的用户体验,而不是显示默认的浏览器页面。

A.3 技术选型(Service Worker)

A.3.1 使用 Google Workbox 构建 Service Worker

A.3.1.1 什么是 Workbox

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

A.3.1.2 如何使用 Workbox

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

路由和缓存策略

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

import { registerRoute } from 'workbox-routing';
import {
  NetworkFirst,
  StaleWhileRevalidate,
  CacheFirst,
} from 'workbox-strategies';

// Used for filtering matches based on status code, header, or both
import { CacheableResponsePlugin } from 'workbox-cacheable-response';
// Used to limit entries in cache, remove entries after a certain period of time
import { ExpirationPlugin } from 'workbox-expiration';

// Cache page navigations (html) with a Network First strategy
registerRoute(
  // 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 strategy
registerRoute(
  // 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 strategy
registerRoute(
  // 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 个。

预缓存

除了在发出请求时进行缓存(运行时缓存)之外,Workbox 还支持预缓存,即在安装 Service Worker 时缓存资源。有许多资源是非常适合预缓存的:Web 应用程序的起始 URL、离线回退页面以及关键的 JavaScript 和 CSS 文件。

使用一个支持预缓存清单注入的插件(webpack 或 rollup)来在新的 Service Worker 中使用预缓存。

import { precacheAndRoute } from 'workbox-precaching';

// Use with precache injection
precacheAndRoute(self.__WB_MANIFEST);

这个 Service Worker 将在安装时预缓存文件,替换 self.__WB_MANIFEST,其中包含在构建时注入到 Service Worker 中的资源。

离线回退

让 Web 应用在离线工作时感觉更健壮的常见模式是提供一个后退页面,而不是显示浏览器的默认错误页面。通过 Workbox 路由和预缓存,你可以在几行代码中设置这个模式。

import { precacheAndRoute, matchPrecache } from 'workbox-precaching';
import { setCatchHandler } from 'workbox-routing';

// Ensure your build step is configured to include /offline.html as part of your precache manifest.
precacheAndRoute(self.__WB_MANIFEST);

// Catch routing errors, like if the user is offline
setCatchHandler(async ({ event }) => {
  // Return the precached offline page if a document is being requested
  if (event.request.destination === 'document') {
    return matchPrecache('/offline.html');
  }

  return Response.error();
});

如果用户处于离线状态,则返回缓存的离线页面的内容,而不是生成一个浏览器错误。

有了 Workbox,可以利用 Service Worker 的力量来提高性能,并给您的站点提供独立于网络的优秀的用户体验。

A.3.2 自研 Service Worker

自研 Service Worker 更加灵活、可控,但是因为需要考虑到各种兼容,研发成本较高。

A.4 技术实践(Service Worker)

A.4.1 使用 CLI

安装 Workbox:

npm install workbox-cli -D

npx workbox --help

按照引导配置 workbox-config.js

npx workbox wizard

根据配置生成 Service Worker 程序:

npx workbox generateSW workbox-config.js

由于实际静态资源是挂载在 CDN 上面,需要修改预渲染资源的前缀

Workbox CLI - generateSW - Configuration

// A transformation that prepended the origin of a CDN for any URL starting with '/assets/' could be implemented as:

const cdnTransform = async (manifestEntries) => {
  const manifest = manifestEntries.map(entry => {
    const cdnOrigin = 'https://example.com';
    if (entry.url.startsWith('/assets/')) {
      entry.url = cdnOrigin + entry.url;
    }
    return entry;
  });
  return {manifest, warnings: []};
};

更多缓存配置可查阅官方文档

A.4.2 使用 Webpack

安装:

npm install workbox-webpack-plugin --save-dev

Webpack 配置:

// Inside of webpack.config.js:
const WorkboxPlugin = require('workbox-webpack-plugin');
// Version info...
const id = `${page}-v${version}`;

module.exports = {
  // Other webpack config...

  plugins: [
    // Other plugins...

    // WIKI https://developers.google.com/web/tools/workbox/reference-docs/latest/module-workbox-webpack-plugin.GenerateSW#GenerateSW
    new WorkboxPlugin.GenerateSW({
        cacheId: `${id}-gsw`,
        // Do not precache images
        exclude: [/\\.(?:png|jpg|jpeg|svg)$/, 'service-wroker.js'], // Page need refresh twice.
        // target dir
        swDest: `../dist/${page}/service-worker.js`,
        skipWaiting: true,
        clientsClaim: true,
        // Define runtime caching rules.
        // WIKI https://developers.google.com/web/tools/workbox/reference-docs/latest/module-workbox-build#.RuntimeCachingEntry
        // Example https://gist.github.com/jeffposnick/fc761c06856fa10dbf93e62ce7c4bd57
        runtimeCaching: [
          // icon images
          {
            // Match any request that ends with .png, .jpg, .jpeg or .svg.
            urlPattern: /^https:\\/\\/cdn.example.com\\/platform/, // /\\.(?:png|jpg|jpeg|svg)$/,
            // Apply a cache-first strategy.
            handler: 'CacheFirst',
            options: {
              // Use a custom cache name.
              cacheName: `${id}-icon-images`,
              // Only cache 50 images, and expire them after 30 days
              expiration: {
                maxEntries: 50
              },
              // Ensure that only requests that result in a 200 status are cached
              cacheableResponse: {
                statuses: [0, 200]
              }
            }
          },
          // note images & others
          {
            // Match any request that ends with .png, .jpg, .jpeg or .svg.
            urlPattern: /^https:\\/\\/image.example.com/, // /\\.(?:png|jpg|jpeg|svg)$/,
            // Apply a cache-first strategy.
            handler: 'CacheFirst',
            options: {
              // Use a custom cache name.
              cacheName: `${id}-note-images`,
              // Only cache 50 images, and expire them after 30 days
              expiration: {
                maxEntries: 50,
                maxAgeSeconds: 60 * 60 * 24 * 30 // 30 Days
              },
              // Ensure that only requests that result in a 200 status are cached
              cacheableResponse: {
                statuses: [0, 200]
              }
            }
          }
        ]
      });
  ]
};

页面中触发 Service Work:

<script>
// Check that service workers are supported
if ('serviceWorker' in navigator) {
  // Use the window load event to keep the page load performant
  window.addEventListener('load', () => {
    navigator.serviceWorker.register('/service-worker.js');
  });
}
</script>

A.5 添加到桌面方案

A.5.1 manifest.json 配置

{
  "name": "不知不问",
  "short_name": "不知不问",
  "description": "yyds",
  "start_url": "/?entry_mode=standalone",
  "display": "standalone",
  "orientation": "portrait",
  "background_color": "#F3F3F3",
  "theme_color": "#F3F3F3",
  "icons": [
    {
      "src": "https://mazey.cn/fav/logo-dark-circle-32x32.png",
      "sizes": "32x32",
      "type": "image/png"
    },
    {
      "src": "https://mazey.cn/fav/logo-dark-circle-144x144.png",
      "sizes": "144x144",
      "type": "image/png"
    },
    {
      "src": "https://mazey.cn/fav/logo-dark-circle-152x152.png",
      "sizes": "152x152",
      "type": "image/png"
    },
    {
      "src": "https://mazey.cn/fav/logo-dark-circle-180x180.png",
      "sizes": "180x180",
      "type": "image/png"
    },
    {
      "src": "https://mazey.cn/fav/logo-dark-circle-192x192.png",
      "sizes": "192x192",
      "type": "image/png"
    },
    {
      "src": "https://mazey.cn/fav/logo-dark-circle-512x512.png",
      "sizes": "512x512",
      "type": "image/png"
    }
  ],
  "scope": "/"
}

A.5.2 <head> 配置

为网站配置开屏图片、状态栏等。

<!--Mazey's favicon begin-->
<link rel="shortcut icon" type="image/png" href="https://mazey.cn/fav/logo-dark-circle-transparent-144x144.png">
<link rel="icon" type="image/png" sizes="32x32" href="https://mazey.cn/fav/logo-dark-circle-transparent-32x32.png">
<link rel="apple-touch-icon" sizes="144x144" href="https://mazey.cn/fav/logo-dark-circle-144x144.png">
<link rel="apple-touch-icon" sizes="152x152" href="https://mazey.cn/fav/logo-dark-circle-152x152.png">
<link rel="apple-touch-icon" sizes="180x180" href="https://mazey.cn/fav/logo-dark-circle-180x180.png">
<link rel="apple-touch-icon" sizes="192x192" href="https://mazey.cn/fav/logo-dark-circle-192x192.png">
<link rel="apple-touch-icon" sizes="512x512" href="https://mazey.cn/fav/logo-dark-circle-512x512.png">
<!--Mazey's favicon end-->
<!--Mazey's pwa manifest.json-->
<link rel="manifest" href="/wp-content/themes/polestar/manifest.json">
<!-- 开机图片 - begin -->
<!-- iPhone Xs Max (1242px × 2688px) -->
<link rel="apple-touch-startup-image" media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 3)" href="https://i.mazey.net/asset/read/cat-lovers-1242x2688.jpg" sizes="1242x2688">
<!-- iPhone Xr (828px x 1792px) -->
<link rel="apple-touch-startup-image" media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 2)" href="https://i.mazey.net/asset/read/cat-lovers-828x1792.jpg" sizes="828x1792">
<!-- iPhone X, Xs (1125px x 2436px) -->
<link rel="apple-touch-startup-image" media="(device-width: 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3)" href="https://i.mazey.net/asset/read/cat-lovers-1125x2436.jpg" sizes="1125x2436">
<!-- iPhone 8, 7, 6s, 6 (750px x 1334px) -->
<link rel="apple-touch-startup-image" media="(device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2)" href="https://i.mazey.net/asset/read/cat-lovers-750x1334.jpg" sizes="750x1334">
<!-- iPhone 8 Plus, 7 Plus, 6s Plus, 6 Plus (1242px x 2208px) -->
<link rel="apple-touch-startup-image" media="(device-width: 414px) and (device-height: 736px) and (-webkit-device-pixel-ratio: 3)" href="https://i.mazey.net/asset/read/cat-lovers-1242x2208.jpg" sizes="1242x2208">
<!-- iPhone 5 (640px x 1136px) -->
<link rel="apple-touch-startup-image" media="(device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2)" href="https://i.mazey.net/asset/read/cat-lovers-640x1136.jpg" sizes="640x1136">
<!-- 开机图片 - end -->
<!-- Touch Bar区域显示的网站图标 -->
<link rel="mask-icon" href="https://mazey.cn/fav/logo-dark-circle.svg" color="#F3F3F3">
<!-- 主题色 = manifest.json theme_color -->
<meta name="theme-color" content="#F3F3F3">
<meta name="apple-mobile-web-app-capable" content="yes">
<!-- 状态栏颜色 default/black/black-translucent -->
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<!-- 应用名 -->
<meta name="apple-mobile-web-app-title" content="不知不问">
<!-- 在Windows 8上,我们可以将网站固定在开始屏幕上,而且支持个性化自定义色块icon和背景图片。这个标签是用来定义色块的背景图的。色块图应该为144*144像素的png格式图片,背景透明。 -->
<meta name="msapplication-TileImage" content="https://mazey.cn/fav/logo-dark-circle-transparent-144x144.png">
<!-- 同前一个元数据msapplication-TileImage类似,这个功能是用来设置颜色值,个性化自定义色块(磁贴)icon -->
<meta name="msapplication-TileColor" content="#F3F3F3">

开屏图片尺寸总结:

屏幕尺寸

倍数

图片尺寸

1024x1366(512x683)

x2

2048x2732

834x1194(417x597)

x2

1668x2388

768x1024(384x512)

x2

1536x2048

834x1112(417x556)

x2

1668x2224

810x1080

x2

1620x2160

428x926(214x463)

x3

1284x2778

390x844

x3

1170x2532

375x812

x3

1125x2436

414x896

x3

1242x2688

414x896

x2

828x1792

414x736

x3

1242x2208

375x667

x2

750x1334

320x568

x2

640x1136

附录 B 客户端缓存支持

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

附录 C 客户端离线包支持

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

客户端离线包流程图

附录 D 优化后端接口数据

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

附录 E 优化占用内存

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

Google Chrome Performance

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

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

GPU memory

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

附录 F 预渲染

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

附录 G peerDependencies

G.1 什么是 peerDependencies

peerDependencies 存在于 NPMpackage.json 中,如果项目中依赖模块 X、模块 Y,模块 Y 又依赖模块 X,当模块 X 出现版本冲突的时候,则会打包出 X 的两份代码。在这个模块 Y 里面使用 peerDependencies 引入模块 X,这样当前项目和模块 Y 便会共用这个模块 X。

当在 peerDependencies 引入一个模块后:

  1. 当前的代码将与符合版本条件的模块和谐相处。
  2. 如何这个模块已经存在于项目的 node_modules 中,什么也不提示。
  3. 如何这个模块不存在于项目的 node_modules 中,警告用户缺少此模块,但是不自动安装。

注意: npm i 并不会自动安装 peerDependencies 里的模块,所以开发模块的需要在 devDependencies 同步添加相应模块。

G.2 什么场景下适合引入 peerDependencies

  1. 多份模块代码的重复加载易引发冲突。
  2. 依赖在项目中是可见的。
  3. 可以让开发者自己决定安装哪个版本。

业内应用:

1.ant-design

"peerDependencies": {
    "react": ">=16.9.0",
    "react-dom": ">=16.9.0"
}

2.element-ui

"peerDependencies": {
    "vue": "^2.5.17"
}

G.3 案例

测试项目 test-project 为例,dependencies 为:

# test-project
"dependencies": {
    "m1": "1.0.0",
    "m2": "1.0.0",
    "m-child": "1.0.0"
}

# m1@1.0.0
"dependencies": {
    "m-child": "2.0.0",
}

# m2@1.0.0
"dependencies": {
    "m-child": "3.0.0",
}

npm i 之后,实际的 node_modules 结构是这样的:

node_modules
├── m1 1.0.0
│   └── node_modules
│       └── m-child 2.0.0
├── m2 1.0.0
│   └── node_modules
│       └── m-child 3.0.0
└── m-child 1.0.0

如上所见 m-child 的三个版本都下载到了本地,并且构建打包后包含了三份 m-child 的代码。

为了解决这部分重复代码,在模块 m1 和 m2 中引入 peerDependencies

# m1@1.0.0
"peerDependencies": {
    "m-child": ">=1.0.0"
}

# m2@1.0.0
"peerDependencies": {
    "m-child": ">=1.0.0"
}

npm i 之后,实际的 node_modules 结构:

node_modules
├── m1 1.0.0
├── m2 1.0.0
└── m-child 1.0.0

test-project、m1、m3 三个部分将和谐共用 m-child@1.0.0,构建打包后也只有一份 m-child

参考

  1. Resource Hints – What is Preload, Prefetch, and Preconnect?
  2. 渐进式 Web 应用(PWA) | MDN
  3. What is the difference between GPU memory and CPU memory?
  4. 使用内存性能分析器查看应用的内存使用情况

版权声明

本博客所有的原创文章,作者皆保留版权。转载必须包含本声明,保持本文完整,并以超链接形式注明作者后除和本文原始地址:https://blog.mazey.net/optimize-frontend-performance

(完)

本站文章资源均来源自网络,除非特别声明,否则均不代表站方观点,并仅供查阅,不作为任何参考依据!
如有侵权请及时跟我们联系,本站将及时删除!
如遇版权问题,请查看 本站版权声明
THE END
分享
二维码
海报
【腾讯云前端性能优化大赛】前端性能和加载体验优化实践(附:PWA、离线包、内存优化、预渲染)
特别是对于首屏资源加载中的白屏时间,用户等待的时间就越长,用户感知到页面的速度就越慢。麻省理工学院的 Richard Larson 在讲话中指出,“人类将被动等...
<<上一篇
下一篇>>