交互时`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 的投入。

相关推荐
喵叔哟23 分钟前
重构代码之取消临时字段
java·前端·重构
还是大剑师兰特1 小时前
D3的竞品有哪些,D3的优势,D3和echarts的对比
前端·javascript·echarts
王解1 小时前
【深度解析】CSS工程化全攻略(1)
前端·css
一只小白菜~1 小时前
web浏览器环境下使用window.open()打开PDF文件不是预览,而是下载文件?
前端·javascript·pdf·windowopen预览pdf
方才coding1 小时前
1小时构建Vue3知识体系之vue的生命周期函数
前端·javascript·vue.js
阿征学IT1 小时前
vue过滤器初步使用
前端·javascript·vue.js
王哲晓1 小时前
第四十五章 Vue之Vuex模块化创建(module)
前端·javascript·vue.js
丶21361 小时前
【WEB】深入理解 CORS(跨域资源共享):原理、配置与常见问题
前端·架构·web
发现你走远了1 小时前
『VUE』25. 组件事件与v-model(详细图文注释)
前端·javascript·vue.js
Mr.咕咕1 小时前
Django 搭建数据管理web——商品管理
前端·python·django