你好。是努科助。

今年2月,我们发表了一篇前端性能调优的文章,收到了很多反馈。

现在,前段时间写性能调优文章的人呢?这就是故事。

所以单独开发“努科普罗我在技术书籍排名网站上给 PageSpeed Insights 打了?.

  • 移动的
    PageSpeed Insightsで100点満点の爆速サイト?にした話

  • 计算机
    PageSpeed Insightsで100点満点の爆速サイト?にした話

*分数可能会因频繁装修而有所变动。

我想我尽了最大的努力在一个基础设施配置薄弱的网站上取得如此高的分数,加载第三方代码进行广告和分析,并显示一定数量的丰富内容。

在本文中介绍使用 Nuko Pro 进行性能调整的示例去做。
我希望它对您网站的性能调整有用。

笔记

  • 本文适用于中级和高级用户。即使你是初学者,也可能有些部分读后看不懂,但如果你囤积起来再读一遍,我想你的理解会再次发生变化。
  • 为了清楚起见,我将它们分为几类,但有一些微妙的类别,所以还不错。
  • 示例代码多为ReactTailwindCSS
  • 我不会详细介绍库和 API。
  • 我不确定这是否直接有助于提高您的 PageSpeed Insights 分数,但我在这里介绍它,希望它可以帮助您进行性能调整。

先决条件

在介绍性能调优的例子之前,先写下使用的库和基础设施环境等先决条件。

  • 版本
    • 反应 18.2.0
    • Next.js 12.3.1
    • Node.js 16
    • 顺风 CSS 3.1.8
  • 基础设施
    • Cloud Run
      • 地区是asia-northeast1
      • memory=512Mi, cpu=1
  • 其他
    • 根据亚马逊附属公司的条款,亚马逊提供的图片(jpg?)按原样使用。
      • 所以没有优化,例如将图像转换为 WebP。
    • 为 Google Analytics 和 Google Adsense 读取第三方代码。

HTML/CSS 版

考虑您的图像加载策略

首先,将高/低加载优先级图像分开,如下所示:

  • 高优先级
    • 在第一个视图中显示的主要内容图像。
  • 低优先级
    • 图像未在第一个视图中显示
    • 出现在第一个视图中但不是主要内容的图像。

以这种方式对图像进行分类后,正确使用html如下。

<!-- 優先度の高い画像 -->
<!-- link タグは head タグの中で -->
<link
  rel='preload'
  as='image'
  href='...'
  fetchpriority='high'
>
<img
  src='...'
  decoding='async'
  loading='eager'
  fetchpriority='high'
>

<!-- 優先度の低い画像 -->
<img
  src='...'
  decoding='async'
  loading='async'
  fetchpriority='low'
>

有两点,所以我会详细解释。

① 使用链接标签提前加载图片

link 标签rel=preload 允许您在页面加载过程的早期预加载资源,例如图像。
对于加载优先级高的图片,指定link标签rel=preload提前读取。
您也可以指定fetchpriority,稍后将介绍。

② 使用decodingloadingfetchpriority属性

每个属性将分别解释。

解码

您可以指定是同步还是异步处理图像解码。
这个属性是无论图像加载优先级如何,都指定异步 (decoding=async)我试着
如果您使用同步处理,浏览器的其他处理将被阻止。是。

加载

您可以指定立即或延迟加载图像等。
立即加载 (loading='eager') 用于在快速视图中显示的高优先级图像,延迟加载其他低优先级图像 (loading='async')

获取优先级

适用于一些现代浏览器但,可以对浏览器指定获取图像等的优先级.
为具有高加载优先级的图像指定fetchpriority='high',为具有低加载优先级的图像指定fetchpriority='low'

不要在 TailwindCSS 中指定额外的类

TailwindCSS 只为您使用的类定义样式.
反过来,将类作为 CSS 交付给客户端,即使它们只在一个地方使用将会
因此,尽可能尝试使用现有代码已经使用的类。

在 CSS 中使用 contain 属性

当样式发生变化时,浏览器会重新计算整个页面的布局。
contain 属性告诉浏览器仅在有限区域内重新计算。

如果您使用 TailwindCSS,contain 属性默认不可用。

const plugin = require('tailwindcss/plugin');

module.exports = {
  // ....
  plugins: [
    plugin(function ({ addUtilities }) {
      addUtilities({
        '.contain-strict': {
          contain: 'strict',
        },
        '.contain-content': {
          contain: 'content',
        },
        '.contain-size': {
          contain: 'size',
        },
        '.contain-layout': {
          contain: 'layout',
        },
        '.contain-style': {
          contain: 'style',
        },
        '.contain-paint': {
          contain: 'paint',
        },
      });
    }),
  ],
};

如果不仔细应用样式,样式会崩溃,所以要小心?。

缩小 CSS

使用cssnano 缩小CSS。
如果您使用的是 TailwindCSS,官方文档描述了如何操作。

用非图像方法替换图标

不要内嵌 SVG 或从外部请求非复杂图标的图像使用 HTML/CSS 显示图标我能做到。
请参阅下面的文章以获取汉堡包按钮的示例。

此外,如果您不想进行精心设计,表情符号替代品也考虑一下。

<div className='before:content-["?"]'>左に?が表示される</div>

删除不必要的 DOM 和样式

我会尽快找到它。
特别是对于列表显示,臃肿的 DOM 和样式往往会成为性能瓶颈,因此我们将仔细审查实现和重构。

浏览器 API

不要使用IntersectionObserver 在视口之外绘制组件

您可以使用IntersectionObserver 检查目标 DOM 是否在特定区域内.
努科普罗基本上隐藏了不在视口中的 DOM,并进行了调整,以便在进入视口之前绘制 DOM。

在用户滚动之前不要在视口之外绘制列表

在 NukoPro 技术书籍列表页面,初始加载时只绘制了两个列表项.
这是因为在用户的第一个视图中显示两个项目就足够了。
用户滚动时绘制其他项目去做。
(虽然被画了,但被IntersectionObserver画成隐藏状态)

为了加快这样的初始加载,第一个列表项被渲染得越少越好。

<!-- 初期描画 -->
<ul>
  <li>アイテム1</li>
  <li>アイテム2</li>
</ul>

<!-- ユーザーがスクロールしたら他のアイテムも描画 -->
<!-- display を hidden にしているのは Core Web Vitals の CLS 対策 -->
<!-- IntersectionObserver を使ってビューポート手前までスクロールされたらアイテム3やアイテム4を表示 -->
<ul>
  <li>アイテム1</li>
  <li>アイテム2</li>
  <li style="display: hidden">アイテム3</li>
  <li style="display: hidden">アイテム4</li>
  <li style="display: hidden">アイテム5</li>
</ul>

requestIdleCallbackScheduler.postTask 用于低优先级任务

在撰写本文时,API requestIdleCallback 在 Chrome 等中可用,Safari 等除外。
这允许我们定义浏览器空闲时我们想要做什么。

另一方面,Scheduler API 的postTask 方法也可用于 Chrome 等,但在撰写本文时,Safari 等除外。
postTask 可以指定任务执行优先级。

发送分析数据、加载低优先级模块、设置缓存等都是使用这些 API 实现的。正在做。

还有,因为不影响后续的重要任务,不超过 50 毫秒我通过像这样拆分任务来使用这些 API:

反应版

优化组件渲染

使用memouseCallbackuseMemo稳定调优,避免不必要的渲染运行我将会继续。
为了最大化 memoization 的效果,创建一个组件,考虑数据是否可以同时更新。去做。

// Before

import FavoriteIcon from 'FavoriteIcon';

function Article({ title, description, isFavorite }) {
  return (
    <>
      <span>{title}</span>
      <span>{description}</span>
      <FavoriteIcon isFavorite={isFavorite} />
    </>
  )
}

// After

import { memo } from 'React';
import FavoriteIcon from 'FavoriteIcon';
// 記事タイトルと記事説明文は同時にデータ変更されるので別コンポーネント化
import ArticleSummary from 'ArticleSummary';

// メモ化
const MemorizedFavoriteIcon = memo(FavoriteIcon);
const MemorizedArticleSummary = memo(ArticleSummary);

function Article({ title, description, isFavorite }) {
  return (
    <>
      <ArticleSummary title={title} description={description} />
      <MemorizedFavoriteIcon isFavorite={isFavorite} />
    </>
  )
}

即使记忆了任何东西,也会生成一个缓冲区,所以考虑渲染树如果父组件可以进行memoization,则不要memoize子组件会这样做。

并且尽可能多,如以下示例中所述。使用children 实现.
通过做这个即使触发了由于状态更改而重新渲染,作为子级传入的组件也不会重新渲染.

function Timer({ children }) {
  const [isTimeout, setIsTimeout] = useState(false);

  useEffect(() => {
    const timer = setTimeout(() => setIsTimeout(true), 3000);
    return () => clearTimeout(timer);
  }, [])

  return (
    {/* 3 秒後に再レンダリングが走っても、 children は再レンダリングされない */}
    <div style={{ backgroundColor: isTimeout ? 'red' : 'white' }}>
      {children}
    </div>
  )
}

删除不必要的useStateuseEffect

我会稳步删除不必要的useStateuseEffect

例如,如果您同时更新状态,则删除 useState 将被归为一个状态。

// Before

const [isLoading, setIsLoading] = useState(true);
const [isFetchSuccess, setIsFetchSuccess] = useState();

useEffect(() => {
  setIsLoading(true);
  fetch('...')
    .then(() => setIsFetchSuccess(true))
    .catch(() => setIsFetchSuccess(false))
    .finally(() => setIsLoading(false));
}, [])

// After

const [fetchStatus, setFetchStatus] = useState();

useEffect(() => {
  setFetchStatus('loading');
  fetch('...')
    .then(() => setFetchStatus('success'))
    .catch(() => setFetchStatus('failed'));
}, [])

删除useEffect,例如在父组件中指定key,可以删除useEffect

// Before

function Article({ articleId }) {
  const [comment, setComment] = useState('');

  useEffect(() => {
    setComment('');
  }, [articleId]);
}

// After

function UserBestArticle({ articleId }) {
  // key を指定すると、 key が変わるたびにリセットされる
  return <Article key={articleId} articleId={articleId} />
}

function Article({ articleId }) {
  const [comment, setComment] = useState('');
  // useEffect は不要
}

删除 useStateuseEffect,参考 React 官方文档。

使用 ref 缓存 IntersectionObserver 实例

IntersectionObserver我在《使用IntersectionObserver防止绘制视口外的组件》中提到过,但是每次重新渲染时生成IntersectionObserever是没用的。

所以无论使用 ref 进行渲染,都保留 IntersectionObserver一定要保持

const observerRef = useRef();

useEffect(() => {
  // ...
  if (!observerRef.current) {
    observerRef.current = new IntersectionObserver(callback, option);
  }
  // ...
}, [dependencies])

只需要注册一次事件监听器

如果你只是简单地使用 React 的自定义钩子到addEventListener,事件监听器将被注册的次数与自定义钩子一样多。
例如,准备一个自定义的钩子来计算视口的水平长度并确定它是否是移动的。

export default function useDeviceDetect() {
  // サーバーサイドでのレンダリングを考慮して初期値は window.innerWidth ではなく 0
  const [width, setWidth] = useState(0);

  const handleWindowResize = () => {
    setWidth(window.innerWidth);
  };

  useEffect(() => {
    !width && handleWindowResize();
  }, [width]);

  useEffect(() => {
    window.addEventListener('resize', handleWindowResize, { passive: true });
    return () => {
      window.removeEventListener('resize', handleWindowResize);
    };
  }, []);

  const isDetect = !!width;
  const isMobile = width < 1024; // ブレークポイントは 1024 px
  return [isDetect, isMobile];
}

如果useDeviceDetect在5个地方使用,则将注册5个'resize'handleWindowResize
(许多人可能会在工作中发现类似的代码并意外地执行它。)

本来'resize'注册handleWindowResize一次就够了。
这可以通过使用 React 的 Context 在应用程序中仅注册一次事件并让每个组件通过 Context 查看它是否可移动来解决。

使用更少的库

尽量减少库的使用并减少脚本大小会这样做。
例如,React 考虑了RecoilRedux 等状态管理库,努科普罗我什么都没用过。
如果你想全局管理状态,React 的 Context 就足够了。

我也是从前面取,所以我在考虑SWRReact Query,但是我没有在Nukopuro中使用它,因为从前面取的情况并不多。
我会尽力自己缓存响应。

这样,我们开发时尽可能不使用库。

Next.js 版

只预取重要的东西

Next.js 的 Link 组件很有用。当链接显示在视口中时,它会自动从链接目标获取必要的数据。
但,对于初始渲染中显示的链接较多的页面,进行数据获取的处理,反之,浏览器就会有负担。.

在 Nukopuro 中,仅对非常重要的链接启用预取,例如在技术书籍排名中排名第一的项目。正在做。
另一个原因是通过减少请求来降低服务器成本(因为它是个人开发......)。

import Link from 'next/link';

function PrefetchLink({ priority = false, ...props }) {
  return <Link prefetch={priority} {...props} />
}

不要使用下一个/图像

我不使用Vercel或Akamai等云图片优化服务,在一定程度上可以自己写图片优化代码,所以没有特别使用next/image。
根据您正在开发的服务,我认为您不必强制使用 next/image 来增加捆绑包的大小。

使用 next/dynamic 进行代码拆分

从根本上说在需要之前不要加载额外的脚本.

例如,在从服务器获取和执行脚本之前,页脚组件将滚动到视图中。
结合“不要使用 IntersectionObserver 在视口之外渲染组件”中介绍的IntersectionObserver 实现。

const Footer = dynamic(() => import('Footer'));

function LazyLoadFooter() {
  return (
   {/* ビューポートに入ったら読み込む */} 
    <LazyLoad>
      <Footer />
    </LazyLoad>
  )
}

另外,React.lazy/React.Suspense也可以代替next/dynamic

在构建时获取图像重定向 URL 和大小

如果您按原样使用亚马逊附属图片 URL,则会发生重定向。
这就是为什么,构建时提前打图片URL(next build)获取重定向目标URL.

另外,最好提前知道大小以优化图像的渲染,但是由于它是外部资源,因此图像的大小是未知的。
这就是为什么,在构建时计算大小以及重定向 URL我会保留它。
image-size 用于大小计算。

这样,通过预先获取有关图像的信息来优化图像显示。

亚马逊,抱歉问得太多了。

将构建时所需的数据保存在静态文件中

Next.js 将每个页面需要的数据输出到一个静态文件中,但是对于不依赖于页面的东西,比如全局需要的数据,不做静态文件。
在执行next build之前将必要的数据输出到静态文件我会保留它。
输出到静态文件时,仅将数据处理为应用程序所需的信息,并尽可能减小文件大小。.

通过做这个,立即显示数据,无需向后端数据库发出请求并返回数据能够。

// Before
function BestArticles() {
  const [bestArticles, setBestArticles] = useState([]);

  useEffect(() => {
    fetch('バックエンドのAPI URL')
       .then(( { articles } ) => setBestArticles(setBestArticles));
  }, [])

  return (
   <ul>
     {bestArticles.map(article => (
       <li key={article.id}>{article.title}</li>
     ))}
   </ul>
  )
}

// After
// 事前に静的ファイルにしておく
import bestArticles from './best_articles.json';

function BestArticles() {
  return (
   <ul>
     {bestArticles.map(article => (
       <li key={article.id}>{article.title}</li>
     ))}
   </ul>
  )
}

// 読み込み戦略によってはこういうのもあり
function BestArticles() {
  const [bestArticles, setBestArticles] = useState([]);

  useEffect(() => {
    const importBestArticles = () => {
      import('./best_articles.json')
       .then(({ default: bestArtcles }) => setBestArticles(bestArtcles));
    }
     // ページが完全に読み込みが完了してから遅延的に静的ファイルを読み込む
    if (document.readyState === 'complete') {
      importBestArticles();
    } else {
      window.addEventListener('load', importBestArticles);
    }
  }, [])

  return (
   <ul>
     {bestArticles.map(article => (
       <li key={article.id}>{article.title}</li>
     ))}
   </ul>
  )
}

适用于现代浏览器的捆绑包

根据您的构建工具,将browsersList 字段添加到package.json 将优化您想要服务的浏览器的捆绑包并减少脚本大小。
虽然它是 Next.js 中的一个实验性功能,但您可以使用这个browsersList 的功能.

在 Next.js 配置文件中指定以下设置。

// next.config.mjs
export default {
  // ...
  experimental: {
    browsersListForSwc: true,
    legacyBrowsers: false,
  }
  // ...
}

这样package.jsonbrowsersList 字段将起作用。

  "browserslist": [
    "> 1% in JP",
    "not IE 11"
  ]

第三方代码

大多数网站,例如 Google Analytics 和 Google Adsense,都会加载第三方代码进行分析和广告展示。
试过性能调优的人都会明白,但我觉得看这个第三方代码总是一堵墙。
现在起”努科普罗我将介绍在“.

使用 Partytown 将 Google Analytics 加载委托给另一个线程

每个人派对镇你知道那个图书馆吗?

通过使用 Partytown,您可以将 Google Analytics 等第三方代码加载到 WebWorker 中。.
这将主线程可以在不被加载第三方代码阻塞的情况下做其他重要的工作.

如果您使用的是 Next.js,则可以通过在 next/script 中指定 strategy='worker' 来使用 Partytown。当然,您也可以将它与其他框架一起使用。

Partytown 本身是实验性的,所以需要考虑是否引入。

用户在加载 Google Adsense 之前滚动

如果您使用 Google Adsense,您可能知道Google Adsense 脚本加载对性能有很大影响.

位于努科普罗在智能手机上访问时,在用户滚动后加载 Google Adsense我正在做。
这加快了初始负载。

其他

将库更新到最新版本

从性能的角度来看,将库更新到最新版本也很重要。
例如,React 在对 v18 的重大更新期间改进了内存是在做努科普罗但内存使用率提高了约 20%它完成了。
其他,Chart.js 也可以在 v3 中使用 Tree Shaking也有例子。

这样,将库更新到最新版本也会带来性能提升。

浏览捆绑的文件

当您查看捆绑的文件时,您可能会注意到“哦???”。

例如,我注意到一个带有巨大字符串JSON.parse 的 js 文件在第一次加载时被传递到浏览器。
我想在第一次读取时尽可能避免像JSON.parse这样的同步处理,并且由于该文件最初不是第一次读取所必需的,所以我尝试通过使用next/dyamic进行代码拆分第一次不读取它。

不料查看捆绑的文件也很重要是。

额外:我尝试但放弃的措施

建立你自己的派对小镇

Partytown 使用 ServiceWorker 与主线程上的 WebWorker 进行通信。
本来我是在 Nukopro 自己写的 ServiceWorker 代码,所以就像 Partytown 一样,你可以使用现有的 ServiceWorker 从 WebWorker 操作 DOM,对吧?我想,并挑战自己实施Partytown。

目前,我使在 WebWorker 上操作 DOM(例如,执行getBoundingClientRect)成为可能。
但是,我放弃了它,因为 Proxy 的递归地狱导致代码很糟糕,并且没有明显的性能影响。

如果你是一个极客,想在 WebWorker 上使用 windowdocument 来操作 DOM!微笑

加载网络字体

Webfonts 也是性能调优的一个难题(尤其是日本的 webfonts)。
大型资源必须预先加载。

Next.js字体优化或者字体源使用名为的 npm 库进行自托管我也试过了,但是PageSpeed Insights给我的印象是很难突破90分关卡。
(顺便说一句,后者Fontsource 得分更高)

我们权衡了设计和性能,优先考虑性能,放弃了 Webfonts。

在 Partytown 中使用 Atomics 和 SharedArrayBuffer 制作模式

Partytown 谈到了使用 ServiceWorker 和 WebWorker 来加载脚本。
实际上如果您使您的站点“跨域隔离(crossOriginIsolated)”,您还可以使用 Atomics 和 SharedArrayBuffer 来加载脚本。

实际上努科普罗但是,当我使用 Next.js 中间件进行“跨域分离”并使用 Atomics 和 SharedArrayBuffer 将脚本读取更改为 Partytown 时,我的 PageSpeed Insights 分数暴涨!做过。

我谈到了放弃加载 Webfonts,即使加载了 webfonts,PageSpeed Insights 得分也超过 100曾是。

相反,请使用您自己网站之外的资源,例如所有图片和广告消失(无法加载)?。
在撰写本文时,由于大多数资源获取目的地不支持跨域分离,因此很难将其投入实际使用。

迁移到 Preact

Preact 是一个轻量级的 React 库。

如果你在一定程度上有使用 React 进行性能调优的经验,你可能知道,从性能的角度来看,加载react-dom 会受到影响.
解决react-dom 过度加载的一种方法是迁移到轻量级库Preact

所以我切换到 Preact 来减小脚本大小,但是我在渲染无限滚动列表时被无法解释的行为所困扰,所以我被迫切换回 React。

闭幕致辞

性能调优的例子如何?

其实有些部分我想用更多的代码示例来详细解释一下,但它会占用整篇文章,所以有一些简短的解释。
另外,我自己也忘记了过去的措施,有些地方我不能一一介绍。

我想再次更新这篇文章或将其包装在单独的文章中。
(有货时我们会通知您!)

这篇性能调优文章会不定时更新,敬请关注!

我们也有推特,所以请关注我们!

希望这可以帮助您改进您的网站!
谢谢收看!


原创声明:本文系作者授权爱码网发表,未经许可,不得转载;

原文地址:https://www.likecs.com/show-308630004.html

相关文章:

  • 2021-09-17
  • 2021-05-10
  • 2022-01-09
  • 2021-06-18
  • 2021-10-26
  • 2021-11-05
  • 2021-09-14
猜你喜欢
  • 2021-09-13
  • 2022-12-23
  • 2021-09-26
  • 2021-04-24
  • 2021-04-11
  • 2021-10-31
  • 2021-11-25
相关资源
相似解决方案