交互时`import`内容模式以及常见场景

原文:www.patterns.dev/vanilla/imp...

标题:Import On Interaction

作者:patterns.dev
简介:当用户与需要它的 UI 交互时,懒加载非关键资源

你的页面可能包含一些当前不立即需要的组件或资源的代码或数据。例如,用户界面中的某些部分,除非用户点击或在页面上滚动,否则看不到。这适用于你编写的各种代码,同时也适用于第三方小工具,比如视频播放器或聊天小工具,通常需要点击按钮才能展示主界面。

如果这些资源很耗费资源,并且急切地加载(即立刻加载),可能会阻塞主线程,延迟用户与页面上更关键部分的交互时间。这可能会影响到交互准备性指标,如首次输入延迟(First Input Delay)、总阻塞时间(Total Blocking Time)和可交互时间(Time to Interactive)。与其立即加载这些资源,不如在更合适的时机进行加载,比如:

  • 当用户首次点击与该组件进行交互时
  • 滚动该组件进入视图时
  • 或者延迟加载该组件,直到浏览器处于空闲状态(通过requestIdleCallback)。

以高层次来看,加载资源的不同方式包括:

  • 急切加载 - 立即加载资源(加载脚本的常规方式)
  • 懒加载(基于路由) - 当用户导航到路由或组件时加载
  • 懒加载(通过交互) - 当用户点击 UI 时加载(例如显示聊天)
  • 懒加载(在视口内) - 当用户向组件滚动时加载
  • Prefetch - 在需要之前加载,但在关键资源加载后
  • Preload - 急切加载,具有更高的紧急程度

对于项目本身代码代码,只有在无法在交互之前预取资源时才应该进行交互加载。然而,这种模式在第三方代码中非常相关,通常希望将其推迟到以后的时间点,如果它对非关键部分很重要。这可以通过许多方式实现(推迟到交互时,直到浏览器处于空闲状态或使用其他启发式方法)。

在交互时懒加载功能代码是许多上下文中使用的一种模式,我们将在本文中介绍。你可能以前在 Google 文档中使用过这种模式,他们通过推迟加载共享功能的脚本来节省 500KB 的加载。

另一个适合使用按需加载的地方是加载第三方小部件。

"假"加载带有外观的第三方 UI

你可能正在导入第三方脚本,并且对它呈现的内容或加载代码的时间的控制较少。实现交互加载的一个选项是直接的:使用外观。外观是一个简单的"预览"或"占位符",用于模拟成本更高的组件,你可以在其中模拟基本体验,例如使用图像或屏幕截图。这是我们在 Lighthouse 团队中一直使用的术语。

当用户单击"预览"(外观)时,将加载资源的代码。这限制了用户在不打算使用某项功能时需要支付的体验成本。同样,立面可以在悬停时预先连接到必要的资源。

第三方资源通常被添加到页面中,而没有充分考虑它们如何适应网站的整体加载。同步加载的第三方脚本会阻止浏览器解析器,并可能延迟冻结。如果可能,应使用 async/defer(或其他方法)加载 3P 脚本,以确保 1P 脚本不会缺少网络带宽。除非它们很关键,否则它们可以很好地使用交互时导入等模式转移到延迟延迟加载。

视频播放器嵌入

"占位符"的一个很好的例子是 Paul Irish 的 YouTube Lite Embed。这提供了一个自定义元素,该元素采用 YouTube 视频 ID,并显示最小的缩略图和播放按钮。点击该元素会动态加载完整的 YouTube 嵌入代码,这意味着从不点击播放的用户无需支付获取和处理该元素的费用。

在一些 Google 网站上的生产中使用了类似的技术。在 Android.com 上,不是急切地加载嵌入的 YouTube 视频播放器,而是向用户显示带有虚假播放器按钮的缩略图。当他们单击它时,会加载一个模态,该模态使用嵌入的全脂 YouTube 视频播放器自动播放视频:

认证场景

应用可能需要支持通过客户端 JavaScript SDK 对服务进行身份验证。这些有时可能很大,JS 执行成本很高,如果用户不打算登录,人们可能宁愿不急于预先加载它们。相反,当用户单击"登录"按钮时,动态导入身份验证库,从而在初始加载期间保持主线程更加自由。

聊天小部件

Calibre 应用程序通过使用类似的门面方法,将基于对讲机的实时聊天的性能提高了 30%。他们仅使用 CSS 和 HTML 实现了一个"假"快速加载实时聊天按钮,单击该按钮时将加载他们的对讲机包。

Postmark 指出,他们的帮助聊天小部件总是被急切地加载,即使它只是偶尔被客户使用。该小部件将提取 314KB 的脚本,比他们的整个主页还要多。为了改善用户体验,他们使用 HTML 和 CSS 将小部件替换为假副本,点击即可加载真实的东西。此更改将交互时间从 7.7 秒缩短到 3.7 秒。

其他

Ne-digital 使用 React 库在用户单击"滚动到顶部"按钮时以动画方式滚动回页面顶部。他们没有急切地为此加载 react-scroll 依赖项,而是在与按钮交互时加载它,节省了 ~7KB:

js 复制代码
handleScrollToTop() {
    import('react-scroll').then(scroll => {
      scroll.animateScroll.scrollToTop({
      })
    })
}

你应该如何做到交互时导入?

在 JavaScript 中,dynamic import() 支持延迟加载模块并返回一个 promise,如果应用得当,它会非常强大。下面是一个示例,其中在按钮事件侦听器中使用动态导入来导入 lodash.sortby 模块,然后使用它。

js 复制代码
const btn = document.querySelector("button");

btn.addEventListener("click", (e) => {
  e.preventDefault();
  import("lodash.sortby")
    .then((module) => module.default)
    .then(sortInput()) // use the imported dependency
    .catch((err) => {
      console.log(err);
    });
});

在动态导入之前,或者对于不太适合的用例,使用基于 Promise 的脚本加载器将脚本动态注入页面也是一种选择(演示登录外观的完整实现):

js 复制代码
const loginBtn = document.querySelector("#login");

loginBtn.addEventListener("click", () => {
  const loader = new scriptLoader();
  loader
    .load(["//apis.google.com/js/client:platform.js?onload=showLoginScreen"])
    .then(({ length }) => {
      console.log(`${length} scripts loaded!`);
    });
});

React

让我们想象一下,我们有一个 Chat 应用程序,它有一个 ,<MessageList>``<MessageInput>和一个<EmojiPicker>组件(由 emoji-mart 提供支持,它是 98KB 缩小和 gzip 压缩的)。在初始页面加载时急切地加载所有这些组件是很常见的。

jsx 复制代码
import MessageList from './MessageList';
import MessageInput from './MessageInput';
import EmojiPicker from './EmojiPicker';

const Channel = () => {
  ...
  return (
    <div>
      <MessageList />
      <MessageInput />
      {emojiPickerOpen && <EmojiPicker />}
    </div>
  );
};

通过代码拆分来分解这项工作的负载相对简单。React.lazy 方法可以轻松地使用动态导入在组件级别对 React 应用程序进行代码拆分。React.lazy 函数提供了一种内置方法,可以将应用程序中的组件分成单独的 JavaScript 块,只需很少的跑腿工作。然后,当您将它与 Suspense 组件耦合时,您可以处理加载状态。

jsx 复制代码
import React, { lazy, Suspense } from 'react';
import MessageList from './MessageList';
import MessageInput from './MessageInput';

const EmojiPicker = lazy(
  () => import('./EmojiPicker')
);

const Channel = () => {
  ...
  return (
    <div>
      <MessageList />
      <MessageInput />
      {emojiPickerOpen && (
        <Suspense fallback={<div>Loading...</div>}>
          <EmojiPicker />
        </Suspense>
      )}
    </div>
  );
};

我们可以将这个想法扩展到仅在 Emoji 图标被单击时导入 Emoji Picker 组件的代码<MessageInput>

jsx 复制代码
import React, { useState, createElement } from "react";
import MessageList from "./MessageList";
import MessageInput from "./MessageInput";
import ErrorBoundary from "./ErrorBoundary";

const Channel = () => {
  const [emojiPickerEl, setEmojiPickerEl] = useState(null);

  const openEmojiPicker = () => {
    import(/* webpackChunkName: "emoji-picker" */ "./EmojiPicker")
      .then((module) => module.default)
      .then((emojiPicker) => {
        setEmojiPickerEl(createElement(emojiPicker));
      });
  };

  const closeEmojiPickerHandler = () => {
    setEmojiPickerEl(null);
  };

  return (
    <ErrorBoundary>
      <div>
        <MessageList />
        <MessageInput onClick={openEmojiPicker} />
        {emojiPickerEl}
      </div>
    </ErrorBoundary>
  );
};

Vue

在 Vue.js 中,类似的交互导入模式可以通过几种不同的方式完成。一种方法是使用包装在函数中的动态导入来动态导入 Emojipicker Vue 组件,即 () () => import("./Emojipicker"))。通常这样做会让 Vue.js 在需要渲染组件时延迟加载组件。

然后,我们可以在用户交互后面限制延迟加载。在选择器的父 div 上使用条件 v-if,通过单击按钮进行切换,然后我们可以在用户单击时有条件地获取和呈现 Emojipicker 组件。

vue 复制代码
<template>
  <div>
    <button @click="show = true">Load Emoji Picker</button>
    <div v-if="show">
      <emojipicker></emojipicker>
    </div>
  </div>
</template>

<script>
  export default {
    data: () => ({ show: false }),
    components: {
      Emojipicker: () => import("./Emojipicker"),
    },
  };
</script>

大多数支持动态组件加载的框架和库(包括 Angular)都应该可以实现交互导入模式。

作为渐进式加载的一部分的 first-party code的交互时导入

在交互时加载代码也恰好是 Google 如何处理大型应用程序(如 Flights 和 Photos)中渐进式加载的关键部分。为了说明这一点,让我们看一下 Shubhie Panicker 之前提出的一个例子。

想象一下,一位用户正在计划去印度孟买旅行,他们访问 Google 酒店查看价格。这种交互所需的所有资源都可以预先加载,但如果用户没有选择任何目的地,则地图所需的 HTML/CSS/JS 将是不必要的。

在最简单的下载场景中,假设 Google Hotels 正在使用朴素的客户端渲染 (CSR)。所有代码都将预先下载和处理:HTML,然后是JS,CSS,然后获取数据,只有在我们拥有所有内容后才能呈现。但是,这会使用户等待很长时间,而屏幕上不会显示任何内容。很大一部分 JavaScript 和 CSS 可能是不必要的。

接下来,假设此体验已移至服务器端渲染 (SSR)。我们将允许用户更快地获得视觉上完整的页面,这很好,但是在从服务器获取数据并且客户端框架完成冻结之前,它不会是可交互式的。

SSR 可能是一种改进,但用户可能会有一种不可思议的山谷体验,页面看起来已经准备好了,但他们无法点击任何内容。有时这被称为愤怒点击,因为用户往往会在沮丧中一遍又一遍地点击。

回到 Google Hotels 搜索示例,如果我们稍微放大 UI,我们可以看到,当用户单击"更多过滤器"以找到正确的酒店时,该组件所需的代码就会被下载。

最初只下载非常少的代码,除此之外,用户交互决定了何时发送哪些代码。

让我们仔细看看这个加载场景。

交互驱动的后期加载有许多重要方面:

  • 首先,我们最初下载最少的代码,以便页面在视觉上快速完成。
  • 接下来,当用户开始与页面交互时,我们使用这些交互来确定要加载的其他代码。例如,加载"更多过滤器"组件的代码。
  • 这意味着页面上许多功能的代码永远不会发送到浏览器,因为用户不需要使用它们。

我们如何避免失去早期点击?

在这些 Google 团队使用的框架堆栈中,我们可以尽早跟踪点击次数,因为 HTML 的第一个块包含一个小型事件库 (JSAction),用于在框架引导之前跟踪所有点击次数。这些事件用于两件事:

  • 根据用户交互触发组件代码的下载
  • 在框架完成引导时重播用户交互

可以使用的其他潜在启发式方法包括加载组件代码:

  • 空闲时间后的时间段
  • 在用户将鼠标悬停在相关 UI/按钮/号召性用语上时
  • 基于基于浏览器信号(例如网络速度、数据保护模式等)的热切程度的滑动比例。

结论

first-party JavaScript 通常会影响 Web 上现代页面的交互准备情况,但它通常会在网络上延迟,这些 JS 来自页面本身或第三方来源,使主线程保持繁忙。

一般情况下,避免在文档头中使用同步的第三方脚本,并在first-party JS 完成加载后加载非阻塞的第三方脚本。像交互时导入这样的模式为我们提供了一种方法,可以将非关键资源的加载推迟到用户更有可能需要他们支持的 UI 的程度。

特别感谢 Shubhie Panicker、Connor Clark、Patrick Hulce、Anton Karlovskiy 和 Adam Raine 的投入。

相关推荐
Nan_Shu_61415 分钟前
学习: Threejs (2)
前端·javascript·学习
G_G#23 分钟前
纯前端js插件实现同一浏览器控制只允许打开一个标签,处理session变更问题
前端·javascript·浏览器标签页通信·只允许一个标签页
@大迁世界39 分钟前
TypeScript 的本质并非类型,而是信任
开发语言·前端·javascript·typescript·ecmascript
GIS之路1 小时前
GDAL 实现矢量裁剪
前端·python·信息可视化
是一个Bug1 小时前
后端开发者视角的前端开发面试题清单(50道)
前端
Amumu121381 小时前
React面向组件编程
开发语言·前端·javascript
持续升级打怪中1 小时前
Vue3 中虚拟滚动与分页加载的实现原理与实践
前端·性能优化
GIS之路1 小时前
GDAL 实现矢量合并
前端
hxjhnct1 小时前
React useContext的缺陷
前端·react.js·前端框架
冰暮流星1 小时前
javascript逻辑运算符
开发语言·javascript·ecmascript