一种基于预加载的网站首次访问加速方案

摘要

利用预加载技术实现网站加速打开和交互的方案很多,但是对于特定条件下复杂网站首次访问的加速并没有可行的通用方法。

针对特定条件下的复杂网站加速,提出了一种基于预加载网站资源的加速首次打开方法。通过在前置网站中嵌入 iframe,并在 iframe 中提前把主网站需要的相关资源文件和动态数据缓存至本地,然后当用户进入主网站后优先读取已经缓存的资源和数据并通过 hash 值、时间戳等对比方式确认缓存是否可用,最后,主网站使用缓存打开页面,可以大幅度加快首次打开速度和交互速度。通过多种不同复杂度的网站验证,该方法稳定可靠,并且可以运用到网站加速中。

关键词:预加载;首次访问;网站加速;动态内容;

一、背景技术

随着科学技术的进步,人们越来越多的通过使用 Web 网站来满足生活和工作中的各种需求,对于网站的打开速度要求也越来越高,尤其首次访问打开速度体验会明显影响用户的留存率。

一般网站加载时间每超过1秒就会流失30%的访客,相反,快速加载的网站通常会具有更高的转化率。如沃尔玛研究发现:网站页面打开时间从1S增加到4S时,转化率的下降最为明显,而网站的打开速度每提升1S,转化率会提升2%。

预加载作为加速网站打开的重要技术,预加载的资源可以从缓存中立即获取,而不必依赖于网络请求,这可以显著提高应用的加载速度和响应性。

二、现状分析

目前预加载的实现有多种解决方案,然而

  • 预加载通常发生在网站打开之后,对首次访问起到的加速效果有限
  • 有一些在网页访问之前进行预缓存的方案,但只能缓存静态资源,对于包含动态或个性化内容的页面可能无法准确捕捉和呈现。

因此需要一种可以在网站打开之前同时预加载资源和动态数据的方案,缓存资源用于提高速度,数据用于动态化内容,可以提高用户对网站的使用体验。

2.1 CDN

CDN 技术是一种通过提前将内容加载到全球分布的 CDN 边缘节点,以优化内容的传输速度和用户体验的方法。

但其缩短资源加载速度的效果远不如读取本地缓存,并且通常情况下其只能缓存静态资源而无法缓存动态数据,因此其只能在一定程度上和一定范围内提高网站首次打开速度,无法做到真正的动态加速。

2.2 preload / prefetch / dns-prefetch

preloadprefetch 是两种用于优化网页性能的技术,它们都涉及到在页面加载过程中提前获取资源,dns-prefetch 可以进行 DNS 预解析,提前解析域名对应的 IP 地址。

但使用 preloaprefetchdns-prefetch,因为其必须在网站打开后才开始工作,不可避免会导致初始页面加载时间变长,因为浏览器需要额外的时间来下载这些资源。而且预加载的资源可能不会在用户实际访问相关内容之前被使用,这可能会造成一些不必要的网络开销,尤其是在移动设备上。

2.3 async / defer

asyncdefer 是用于控制脚本加载和执行的两个 HTML <script> 元素的属性。它们对于优化网页性能和脚本执行时机有重要作用。

asyncdefer 允许脚本异步加载,不会阻塞 HTML 解析,可以与其他资源并行加载,提高页面加载速度。异步加载的脚本不会阻塞页面的渲染,因此可以更快地呈现页面内容。但asyncdefer 并非真正意义上的预加载技术,反而是通过延迟非重要资源的加载,从而让主资源更快加载的方案。因其只是改变资源加载顺序而非速度,所以网站全面功能的体验时间并未被缩短。

2.4 Service Worker 预缓存

使用 Service Worker 可以实现离线缓存和资源预加载,确保用户在离线状态下也能访问网站。Service Worker 可以在后台异步预加载资源,以提高用户体验。

但 Service Worker 预缓存的资源需要在首次加载时下载并存储在本地,后续再次访问才可以被利用起来。因此这会导致用户在首次进入网站是初始页面加载时的网络开销增加,尤其是对于包含大量资源的网站。

关于 Service Worker 详细介绍,请点击 juejin.cn/post/706711...

2.5 prerender

<link rel="prerender"> 是一种用于预渲染网页的技术,通过在用户浏览器后台预加载和渲染下一个可能访问的页面,以提高用户体验。然而,其直会提前加载和渲染整个页面,包括图片、样式表、脚本等,对于包含动态或个性化内容的页面,预渲染可能无法准确捕捉和呈现,因为它只能渲染静态的页面快照。

三、发明内容

本方案描述了一种基于预加载网站资源和动态数据的首次访问加速方法

3.1 核心步骤

  1. 通常主体网站前都会有一个前置页面,如果没有可增加一个前置页面,此前置页面足够简单,功能少、依赖少,可以极速打开
  2. 在前置网站中嵌入 iframe 并进行隐藏处理,使用户无法感知
  3. Iframe 中打开主体网站
  4. 主体网站启动后使用 Service Worker 缓存网站静态资源和动态接口数据到本地数据库 IndexDB 和 cache storage中
  5. 用户打开主体网站,对缓存资源和数据查找并进行校验
  6. 如资源和数据存在且可用,直接使用缓存渲染
  7. 如果资源或数据不可用,则放弃对应缓存进行常规拉取

3.2 核心示意图

3.3 前置网站中嵌入 iframe

在网站 html 代码中引入 iframe 标签,并设置隐藏属性

html 复制代码
<style>
    .hidden-iframe {
        display: none;
    }
</style>

<iframe src="https://example.com" class="hidden-iframe"></iframe>

3.4 主体网站使用 Service Worker

在网站 js 代码中启动 Service Worker,为了防止作用域污染,将安装前注销所有已生效的 Service Worker

ts 复制代码
var sp = window.location.protocol + '//' + window.location.host + '/';
if ('serviceWorker' in navigator) {
  navigator.serviceWorker.getRegistrations().then(regs => {
    for (let reg of regs) {
      reg.unregister();
    }
    navigator.serviceWorker
      .register(sp + 'service-worker.js', {
        scope: sp,
      })
      .then(reg => {
        console.log('set scope: ', sp, 'Service Worker instance: ', reg)
      });
  });
}

3.5 Service Worker 中缓存资源和动态数据(使用 Workbox 实现)

ts 复制代码
// 基础配置
import { setCacheNameDetails, skipWaiting, clientsClaim } from 'workbox-core';
// 缓存相关
import { precacheAndRoute } from 'workbox-precaching/precacheAndRoute';
import { registerRoute, setDefaultHandler } from 'workbox-routing';
import {
  NetworkFirst,
  StaleWhileRevalidate,
  CacheFirst,
  NetworkOnly,
} from 'workbox-strategies';
// 插件
import { CacheableResponsePlugin } from 'workbox-cacheable-response/CacheableResponsePlugin';
import { ExpirationPlugin } from 'workbox-expiration/ExpirationPlugin';
// 内置方案
import { pageCache, offlineFallback } from 'workbox-recipes';
// 自定义方法和插件
import { getCacheKeyWillBeUsed, blockStaticSource, blockImageSource } from '@/utils-ts/service-worker'

self.__WB_DISABLE_DEV_LOGS = true;

setCacheNameDetails({
  prefix: 'sw-tools',
  suffix: 'v1',
  precache: 'precache',
  runtime: 'runtime-cache',
});

skipWaiting();
clientsClaim();

/*
 通常当用户访问 / 时,对应的访问的页面 HTML 文件是 /index.html,默认情况下,precache 路由机制会在任何 URL 的结尾的 / 后加上 index.html,这就以为着你预缓存的任何 index.html 都可以通过 /index.html 或者 / 访问到。当然,你也可以通过 directoryIndex 参数禁用掉这个默认行为
 */
precacheAndRoute(self.__WB_MANIFEST, {
  ignoreUrlParametersMatching: [/.*/],
  directoryIndex: null,
});

// URL navigation 缓存
pageCache();

// html 的缓存
// HTML,如果你想让页面离线可以访问,使用 NetworkFirst,如果不需要离线访问,使用 NetworkOnly,其他策略均不建议对 HTML 使用。
registerRoute(new RegExp(/.*.html/), new NetworkFirst());

// 静态资源的缓存
//CSS 和 JS,情况比较复杂,因为一般站点的 CSS,JS 都在 CDN 上,SW 并没有办法判断从 CDN 上请求下来的资源是否正确(HTTP 200),如果缓存了失败的结果,问题就大了。这种我建议使用 Stale-While-Revalidate 策略,既保证了页面速度,即便失败,用户刷新一下就更新了。如果你的 CSS,JS 与站点在同一个域下,并且文件名中带了 Hash 版本号,那可以直接使用 Cache First 策略。
registerRoute(
  blockStaticSource,
  new StaleWhileRevalidate({
    cacheName: 'static-resources',
    plugins: [
      new CacheableResponsePlugin({
        statuses: [0, 200],
      }),
      new ExpirationPlugin({
        maxEntries: 500,
        maxAgeSeconds: 30 * 24 * 60 * 60,
      }),
    ],
  }),
);

// 图片的缓存
// 图片建议使用 Cache First,并设置一定的失效事件,请求一次就不会再变动了。
registerRoute(
  blockImageSource,
  new CacheFirst({
    cacheName: 'images',
    plugins: [
      { cacheKeyWillBeUsed: getCacheKeyWillBeUsed('all') },
      new CacheableResponsePlugin({
        statuses: [0, 200],
      }),
      new ExpirationPlugin({
        maxEntries: 200,
        maxAgeSeconds: 30 * 24 * 60 * 60,
      }),
    ],
  }),
);

// xxxx 接口的缓存
registerRoute(
  /^http(s)?://((dev.)|(test.)|(testing.))?xxxx.net/api/v\d+/(.*)?/xxxx.*/,
  new StaleWhileRevalidate({
    cacheName: 'xxxx_cache',
    plugins: [
      { cacheKeyWillBeUsed: getCacheKeyWillBeUsed('t') },
      new CacheableResponsePlugin({
        statuses: [0, 200],
      }),
      new ExpirationPlugin({
        maxEntries: 2000,
        maxAgeSeconds: 7 * 24 * 60 * 60,
      }),
    ],
  }),
);

// yyyy 接口
registerRoute(
  /^http(s)?://((dev.)|(test.)|(testing.))?xxxx.net/api/v\d+/(.*)?/yyyy.*/,
  new StaleWhileRevalidate({
    cacheName: 'yyyy_cache',
    plugins: [
      { cacheKeyWillBeUsed: getCacheKeyWillBeUsed('t') },
      new CacheableResponsePlugin({
        statuses: [0, 200],
      }),
      new ExpirationPlugin({
        maxEntries: 10000,
        maxAgeSeconds: 20 * 60, // 20分钟
      }),
    ],
  }),
);

ps:Workbox 是 Google Chrome 团队推出的一套 PWA 的解决方案,这套解决方案当中包含了核心库和构建工具,因此我们可以利用 Workbox 实现 Service Worker 的快速开发。

3.6 主网站判断资源和数据是否可用

  1. 判断静态资源是否可用,可以对资源路径添加 hash 值,html 文件引入带 hash 值的文件,Service Worker 拦截请求,通过带 hash 值的路径查找缓存,判断是否存在
html 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Hashed Resource Example</title>
  <!-- 使用带哈希值的静态资源 -->
  <link rel="stylesheet" href="/styles/style-abc123.css" />
</head>
<body>
  <h1>Hashed Resource Example</h1>
  <!-- 页面内容 -->
  <script src="/scripts/script-xyz456.js"></script>
</body>
</html>
csharp 复制代码
self.addEventListener('fetch', (event) => {
  event.respondWith(
    caches.match(event.request).then((response) => {
      // 如果缓存中有匹配的资源,则返回缓存,否则从网络请求
      return response || fetch(event.request);
    })
  );
});
  1. 判断动态数据是否可用,假设认为5分钟以内的数据为可用数据
ts 复制代码
const CACHE_NAME = 'my-api-cache-v1';
const MAX_AGE_SECONDS = 300; // 5分钟

self.addEventListener('fetch', (event) => {
  event.respondWith(
    caches.match(event.request).then((cachedResponse) => {
      if (cachedResponse) {
        // 缓存中有匹配的响应
        const cacheTimestamp = cachedResponse.headers.get('x-cache-timestamp');
        const currentTimestamp = new Date().getTime();

        // 检查时间戳是否在有效期内
        if (currentTimestamp - parseInt(cacheTimestamp, 10) < MAX_AGE_SECONDS * 1000) {
          // 数据在有效期内,返回缓存
          return cachedResponse;
        } else {
          // 数据过期,从网络请求新数据
          return fetchAndCache(event.request);
        }
      } else {
        // 缓存中没有匹配的响应,从网络请求新数据
        return fetchAndCache(event.request);
      }
    })
  );
});

function fetchAndCache(request) {
  return fetch(request).then((response) => {
    // 克隆响应以便能够在响应中使用两次
    const responseToCache = response.clone();

    // 将新数据缓存起来
    caches.open(CACHE_NAME).then((cache) => {
      cache.put(request, responseToCache);
    });

    // 添加时间戳到响应头,以便后续判断数据是否过期
    const timestampedResponse = new Response(response.body, {
      headers: { 'x-cache-timestamp': new Date().getTime() },
    });

    return timestampedResponse;
  });
}

可替代方案

  1. 前置网站可以是任何可以在主产品或主功能之前的页面,比如最常见的进入引导页、产品体验页、新手引导页、产品介绍页或网站入口页等
  2. IndexDB 可以换成 Web sql、localStorage等
  3. Cache Storage 可以换成 Application Cache 等
  4. Service Worker 技术,可以换成 HTTP 缓存等
  5. Hash 值校验可以换成,key 校验、时间戳校验或加密解密校验等校验方式
  6. Iframe 可以换成 webview 或 webContainer 等可以加载网站的容器
  7. 接口动态数据 5分钟以内可用的条件可以根据实际情况自行定义

结论

针对提升网站首次访问速度,本文提出了基于预缓存静态资源和动态数据的实现方案,其主要优势在于:

  1. 不同于其他预缓存方案只能在网站加载后进行,可以在网站访问之前进行预缓存
  2. 不同于其他缓存方案只能从第二次开始加速,新方案可以有效提升网站第一次的访问速度
  3. 不同于其他预缓存方案只能缓存静态资源,新方案同时缓存网站静态资源和数据,使网站打开速度快的同时交互也来的迅速,是一个完整的全缓存
  4. 不同于其他方案对用户有明显感知,新方案对用户几乎无感知

新方案有效的克服了其他方案提速效果不佳、无法提前预加载资源或无法预加载动态数据的缺点,此外新方法已经应用于实际项目中,实验结果表明能够满足实时性要求,是一个可靠的方案。

相关推荐
沛沛老爹24 分钟前
服务监控插件全览:提升微服务可观测性的利器
微服务·云原生·架构·datadog·influx·graphite
J不A秃V头A33 分钟前
Vue3:编写一个插件(进阶)
前端·vue.js
司篂篂1 小时前
axios二次封装
前端·javascript·vue.js
姚*鸿的博客1 小时前
pinia在vue3中的使用
前端·javascript·vue.js
huaqianzkh1 小时前
了解华为云容器引擎(Cloud Container Engine)
云原生·架构·华为云
宇文仲竹2 小时前
edge 插件 iframe 读取
前端·edge
Kika写代码2 小时前
【基于轻量型架构的WEB开发】【章节作业】
前端·oracle·架构
刘某某.2 小时前
使用OpenFeign在不同微服务之间传递用户信息时失败
java·微服务·架构
迪捷软件2 小时前
知识|智能网联汽车多域电子电气架构会如何发展?
架构·汽车
zyhJhon2 小时前
软考架构-层次架构风格
架构