图标

在 React.js 中管理图标的“最佳”方法

翻译自: https://benadam.me/thoughts/react-svg-sprites/

图标渲染技术

在构建用户界面中,图标无处不在。在过去的 7 年里,我一直使用 React 构建 UI,并且尝试了许多不同的技术来管理图标。每种技术都有不同的权衡。我使用的主要技术是:

  1. 图像 + SVG
<img src="icon.svg" />
  1. 内联 SVG
const icon = (
  <svg viewBox="0 0 24 24" width={16} height={16}>
    <path d="M165.9 397.4c0 2-2.3 3.6-5.2 3.6-3.3.3-5.6-1.3-5.6-3.6 0-2 2.3-3.6 5.2-3.6 3-.3 5.6 1.3 5.6 3.6zm-31.1-4.5c-.7 2 1.3 4.3 4.3 4.9 2.6 1 5.6 0 6.2-2s-1.3-4.3-4.3-5.2c-2.6-.7-5.5.3-6.2 2.3zm44.2-1.7c-2.9.7-4.9 2.6-4.6 4.9.3 2 2.9 3.3 5.9 2.6 2.9-.7 4.9-2.6 4.6-4.6-.3-1.9-3-3.2-5.9-2.9zM244.8 8C106.1 8 0 113.3 0 252c0 110.9 69.8 205.8 169.5 239.2 12.8 2.3 17.3-5.6 17.3-12.1 0-6.2-.3-40.4-.3-61.4 0 0-70 15-84.7-29.8 0 0-11.4-29.1-27.8-36.6 0 0-22.9-15.7 1.6-15.4 0 0 24.9 2 38.6 25.8 21.9 38.6 58.6 27.5 72.9 20.9 2.3-16 8.8-27.1 16-33.7-55.9-6.2-112.3-14.3-112.3-110.5 0-27.5 7.6-41.3 23.6-58.9-2.6-6.5-11.1-33.3 2.6-67.9 20.9-6.5 69 27 69 27 20-5.6 41.5-8.5 62.8-8.5s42.8 2.9 62.8 8.5c0 0 48.1-33.6 69-27 13.7 34.7 5.2 61.4 2.6 67.9 16 17.7 25.8 31.5 25.8 58.9 0 96.5-58.9 104.2-114.8 110.5 9.2 7.9 17 22.9 17 46.4 0 33.7-.3 75.4-.3 83.6 0 6.5 4.6 14.4 17.3 12.1C428.2 457.8 496 362.9 496 252 496 113.3 383.5 8 244.8 8zM97.2 352.9c-1.3 1-1 3.3.7 5.2 1.6 1.6 3.9 2.3 5.2 1 1.3-1 1-3.3-.7-5.2-1.6-1.6-3.9-2.3-5.2-1zm-10.8-8.1c-.7 1.3.3 2.9 2.3 3.9 1.6 1 3.6.7 4.3-.7.7-1.3-.3-2.9-2.3-3.9-2-.6-3.6-.3-4.3.7zm32.4 35.6c-1.6 1.3-1 4.3 1.3 6.2 2.3 2.3 5.2 2.6 6.5 1 1.3-1.3.7-4.3-1.3-6.2-2.2-2.3-5.2-2.6-6.5-1zm-11.4-14.7c-1.6 1-1.6 3.6 0 5.9 1.6 2.3 4.3 3.3 5.6 2.3 1.6-1.3 1.6-3.9 0-6.2-1.4-2.3-4-3.3-5.6-2z" />
  </svg>
);
  1. 使用 SVG Sprites 的内联 SVG
const icon = (
  <svg viewBox="0 0 24 24" width={16} height={16}>
    <use href="sprite.svg#icon" />
  </svg>
);

使用 Sprites 的内联 SVG 提供最佳性能以体验权衡。

权衡

我们正在评估的主要权衡是性能与用户体验。在你对我说“好吧,性能是用户体验的一部分”之前,我知道(我同意),让我们继续前进。

技术1:图像 + SVG

当使用我提到的第一种方法时,图像标签引用图像资源(png、svg 等),第一次下载页面时,图标渲染之前会出现闪烁。发生这种情况是因为请求瀑布。首先,浏览器下载 HTML 文档。然后,它发出后续请求以获取页面的所有资源(图像、脚本、样式表等)。引用外部资产的好处是缓存。浏览器(或 CDN)可以缓存资产并在后续请求中引用它。该技术针对后续请求进行了优化,但初始加载体验并不理想。这种技术的另一个缺点是,当在图像标签中引用 svg 时,我们无法使用 CSS 设置 svg 的样式。

技术 2:内联 SVG

第二种技术通常用于解决我刚才提到的问题,即将 svg 内联到 HTML 文档中。当浏览器下载 HTML 文档时,不需要对图像资源进行二次请求,它会立即出现(无闪烁)。另一个好处是现在可以使用 CSS (win + win) 访问内容并设置样式。但这种方法并非没有缺陷。将 SVG 内联到 HTML 文档中会使文档明显变大,并向页面添加元素(这会降低内存性能)。谷歌的优秀人员写了一篇关于这个主题的好文章

第二个陷阱是 SVG 会使 JavaScript 包的大小变得臃肿。在渲染任何内容之前,浏览器必须下载、解析和评估页面上的 JavaScript。将 SVG 包含在您的捆绑包中会让您支付两次费用。您有一个更大的包需要下载和解析(对于某些图标来说,路径数据可能很大,特别是当它们更复杂时),然后渲染的 HTML 会向页面添加大量附加元素(减慢 DOM 遍历)。绝大多数 React 应用程序都使用第二种技术(我过去一直是它的大力支持者),但是在与 react-icons 膨胀相对简单页面的包大小进行斗争之后,我知道必须是一个更好的方式。

注意:有一些方法可以将未使用的图标从捆绑包中删除,但如果你不小心 react-icons 开箱即用,会给你一个 5mb 的 JavaScript 捆绑包。

使用 SVG Sprites 渲染图标

好吧,如果你已经走到这一步,恭喜你,你的生活即将改变。还有第三种选择:SVG Sprites,它可以解决我在这两种方法中提到的大部分问题。

您可能还不够老,无法记住图像精灵(您可能不诚实),但本质上您会将所有图像资源放入单个图像中,然后使用坐标引用特定图像(用于放置图标的位置)在精灵上)。这是一种用于避免大量图像请求的性能技术(HTTP 2 现在已经很大程度上解决了这个问题)。这种技术相似,但又不同。

符号元素

让我向您介绍 <symbol> 元素。符号元素 “用于定义可由 <use> 元素实例化的图形模板对象。” -MDN。当与 <defs> 元素 结合使用时,我们可以用我们的图标构建一个 svg 精灵。

首先,我们创建一个文件 sprite.svg,并添加一个包含 defs 元素和 <symbol><svg> 元素。接下来,我们获取图标(我们将内联的图标),将 svg 替换为符号元素,并为其指定一个 id。身份证很重要!最后,我们将通过将其添加为符号来向精灵添加第二个图标。

例子:

<!-- sprite.svg -->
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
  <defs>
    <symbol viewBox="0 0 24 24" id="icon-1">
      <path
        d="M6.84 5.76L8.4 7.68H5.28l-.72 2.88H2.64l.72-2.88H1.44L0 13.44h3.84l-.48 1.92h3.36L4.2 18.24h2.82l2.34-2.88h5.28l2.34 2.88h2.82l-2.52-2.88h3.36l-.48-1.92H24l-1.44-5.76h-1.92l.72 2.88h-1.92l-.72-2.88H15.6l1.56-1.92h-2.04l-1.68 1.92h-2.88L8.88 5.76zm.24 3.84H9v1.92H7.08zm7.925 0h1.92v1.92h-1.92Z"
      />
    </symbol>
    <symbol viewBox="0 0 24 24" id="icon-2">
      <path
        d="M0 0h24v24H0V0zm22.034 18.276c-.175-1.095-.888-2.015-3.003-2.873-.736-.345-1.554-.585-1.797-1.14-.091-.33-.105-.51-.046-.705.15-.646.915-.84 1.515-.66.39.12.75.42.976.9 1.034-.676 1.034-.676 1.755-1.125-.27-.42-.404-.601-.586-.78-.63-.705-1.469-1.065-2.834-1.034l-.705.089c-.676.165-1.32.525-1.71 1.005-1.14 1.291-.811 3.541.569 4.471 1.365 1.02 3.361 1.244 3.616 2.205.24 1.17-.87 1.545-1.966 1.41-.811-.18-1.26-.586-1.755-1.336l-1.83 1.051c.21.48.45.689.81 1.109 1.74 1.756 6.09 1.666 6.871-1.004.029-.09.24-.705.074-1.65l.046.067zm-8.983-7.245h-2.248c0 1.938-.009 3.864-.009 5.805 0 1.232.063 2.363-.138 2.711-.33.689-1.18.601-1.566.48-.396-.196-.597-.466-.83-.855-.063-.105-.11-.196-.127-.196l-1.825 1.125c.305.63.75 1.172 1.324 1.517.855.51 2.004.675 3.207.405.783-.226 1.458-.691 1.811-1.411.51-.93.402-2.07.397-3.346.012-2.054 0-4.109 0-6.179l.004-.056z"
      />
    </symbol>
  </defs>
</svg>

您可以继续将所有图标定义为该文件中的符号(尽管要注意文件大小,即保持在 125kb 以下)。接下来,我们将创建一个引用我们的精灵的 React 组件。

import { render } from "react-dom";

// keep a list of the icon ids we put in the symbol
const icons = ["icon-1", "icon-2"];

// then define an Icon component that references the
function Icon({ id, ...props }) {
  return (
    <svg {...props}>
      <use href={`/sprite.svg#${id}`} />
    </svg>
  );
}

// In your App, you can now render the icons
function App() {
  return (
    <div className="App">
      {icons.map((id) => {
        return <Icon key={id} id={id} />;
      })}
    </div>
  );
}

const rootElement = document.getElementById("root");
render(<App />, rootElement);

使用元素

在上面的示例中,神奇之处发生在链接到“片段标识符”(也称为我们在符号上定义的 id)的 <use> 元素中。现在,我们有了一个图标组件,可以让我们使用内联 SVG 执行所有操作(例如定义图标的高度、宽度和颜色),但所有路径数据都存在于外部资源中(而不是存在于外部资源中)。 JavaScript 包)。

额外提示

您可以 preload sprite.svg 文件(然后缓存它)来提高性能。 “通过预加载某个资源,您可以告诉浏览器您希望比浏览器发现它更早地获取它,因为您确定它对当前页面很重要。” - web.dev

要预加载精灵,您可以将链接标签添加到文档的头部。

<head>
  <link rel="preload" as="image/svg+xml" href="sprite.svg" />
</head>

根据服务器的配置,您可能需要确保在 sprite.svg 上设置正确的缓存标头,以便浏览器可以适当地缓存它。