likes
comments
collection
share

如何提高Next.js应用评分

作者站长头像
站长
· 阅读数 63

前言

前段时间使用Next.js(13.x)开发一个展示类型的官网,功能比较简单。正式上线前,使用Lighthouse 评测,并做了些性能优化,整体评分达到了90+ ,在此简单总结下。

如何提高Next.js应用评分

在开始说优化之前,我们首先要知道,衡量应用的指标是什么,有了标准我们才能有优化的方向。

Core Web Vitals (核心页面指标)是一种速度指标,是由Google用于衡量网站用户体验的一些指标,并提供了对应的库web-vitals。如下图是web-vitals包含的指标,其中右侧带标志的为核心(Core) 指标,左侧部分为其它指标(Other metrics)

如何提高Next.js应用评分

核心指标是衡量一个网站用户体验的关键维度,测试的工具有很多(核心 Web 指标的测量工具),我们这里以浏览器中的Lighthouse(灯塔)作为检测工具(想了解更详情的关于标准的细节,可以参考这几个网站:web.devWeb AlmanacCore Web Vitals)这里简单列出其中核心指标的评分标准:

良好需要改善不良
LCP< 2.5s2.5s < LCP < 4.0s> 4.0s
CLS< 0.10.1 < CLS < 0.25> 0.25
FID< 100ms100ms < FID < 300ms> 300ms

那么,我们只需要根据这几个指标的重要性排序以及Lighthouse对应的评分来进行调整即可。

正文

开始前先说下整个应用的开发版本:Next.js(13.1.1) + Page Router 进行开发。

当我们一个项目整体开发接近尾声的时候(或者随时都可以)使用 @next/bundle-analyzer,来分析整个应用的包的构成。通过可视化的方式,让我们清晰地看到整个应用的整体结构。

以我使用的版本(13.1.1)为例子,按照@next/bundle-analyzer的使用文档配置后。我们运行 yarn analyze (pkg中script: "ANALYZE=true yarn build")之后,会有以下变化:

  1. 项目根目录的 .next文件夹下,创建一个名称为analyze文件夹。且有三个文件:nodejs.html(服务端渲染),edge.html (边缘计算)和client.html(客户端渲染)。
  2. 并调用默认浏览器打开上面三个html文件,
  3. 在控制台输出对应的build分析信息

如何提高Next.js应用评分

上图中的右下区域,为整个应用build之后bundle文件的信息。我们可以清楚的看到每个路由下文件打包之后的大小(gzip压缩后)。

  1. First Load JS shared by all这一块的文件,是用户首次打开网站任何一个页面都需要加载的common chunks文件,是整个应用共享的:即在A页面加载过这些文件,B页面不需要再次加载。
  2. Size列为:每个页面打包后的文件大小
  3. First Load JS列为:是当前页面在应用初始加载时整体页面大小

Next.js 在终端里通过颜色来提醒我们文件大小是否合适,绿色代表是OK的,红色代表过大,必需要优化。黄色代表需要优化。下图是整个应用analyzer后页面的截图:

如何提高Next.js应用评分

如何提高Next.js应用评分

注:edge.html 我这里是空的,因为没有使用到这个功能,更多关于此功能的使用,请看这里

减小 js 文件的体积

结合上文的截图,从analysis之后的结果我们可以得出以下几点:

  1. 网站首次加载时,加载的总体资源文件过大,172kb
  2. detail页面和list页面首次加载时,总体文件也有点大,为黄色

控制台输出的为客户端的整体文件信息,让我们打开client.html页面,逐步进行分析。

点击左侧的过滤器,在第二项 Filter to initial chunks的下拉菜单中选择pages/index页面,视图显示为该页面的整体依赖结构。

如何提高Next.js应用评分

在进行代码分析前,强烈建议安装vs code 插件Import Cost,它可以在我们写代码时就分析出对应的依赖大小。

减小build之后js文件体积的整体策略如下(结合项目的具体情况进行具体分析):

  1. 尽可能的减小页面首页加载资源的大小,对于非必要的大组件可以采用延迟加载策略
  2. 对于比较大的npm包,可以在函数执行时懒加载执行

这里以目前项目中的代码情况,举几个例子:

  1. 搜索框组件进行延迟加载 ( Dynamic Imports )

在首页页面的右上方有一个搜索功能,在首页时完全没有必要进行加载这些资源。等到用户点击时进行实时加载即可。相关代码如下:

// import SearchInput from "@/components/search"; 👇
// loading 按需进行自定义
const SearchInputDynamic = dynamic(() => import("@/components/search"), {
  loading: () => <p>Loading...</p>
});

// <SearchInput
//   xxxx
// />  👇

 <SearchInputDynamic
  xxxx
/>

打开控制台看下效果:

如何提高Next.js应用评分

  1. 对应在有比较大的npm包也可采用同样的懒加载进行处理
// import { LocomotiveScrollProvider } from "react-locomotive-scroll"; 👇
const LocomotiveScrollProvider = dynamic(() =>
  import("react-locomotive-scroll").then((m) => m.LocomotiveScrollProvider)
);

最后再运行下 yarn analyze,查看构建好之后文件大小,完美一切刚刚好!

如何提高Next.js应用评分

注意:这里根据个人经验有一个点需要说下,并不是所有依赖子组件都使用懒加载处理最好。毕竟你懒加载一次也是一次http请求,如果仅仅为了几kb,增加一个http请求,有些顾此失彼。

个人建议:仅处理一些较大的组件就好,而且如果子组件包含子组件,仅处理最根上的组件就好。在一些需要根据状态变化判断进行加载组件,可以根据大小及决定是否需要懒加载。

跑分综合优化

接下来让我们暂时发布到生产,用Lighthouse跑个分看看。前置条件:

  1. 必须在chrome隐私模式
  2. 禁用所有浏览器插件

如何提高Next.js应用评分

🧐 不出所料的果然很一般!结合上文说的Core Web Vitals衡量标准,我们进行逐个分析。

CLS (Cumulative Layout Shift) 累计布局偏移

Lighthouse中出现了这一条提示一般是页面在加载的过程中出现了布局偏移的情况。在Chrome Develops官网的博客里有很详细的讲解,强烈大家去阅读下。

如何提高Next.js应用评分

出现该问题比较常见有以下几个原因:

  • 没有指定图片宽高,图片加载完成之后造成布局的偏移。
  • 动态加载字体font文件,导致加载完成之后对应位置的闪烁
  • 没有正确的使用css动画,导致性能降低

先来说下,如何处理图片类问题。从<img>这个标签出现以来,我们都知道该元素建议要设置宽高。但是随着响应式布局的流行,我们对于该标签的默认处理方式变成了:

img {
  max-width: 100%; 
  height: auto;
}

在图片加载完成时自然会导致页面布局产生偏移。回到Next.js中,在13.x这个版本中提供了Image组件(应该是和 Chrome Aurora 团队合作开发)来专门进行图片的加载。这里简单说下它对于SSR应用对于图片加载的优化点:

  • 体积优化:自动的为访问设备提供合适的图片大小,并使用现代图片格式, WEBP AVIF (当然这一切,如果使用默认的loader,则会走Next.js内部处理,进行实时处理图片,会非常吃CPU。对于国内使用阿里云、七牛云的OSS用户,需要结合各自的OSS相关特性(裁切、格式转化等),自定义loader实现同等的功能)
  • 视觉稳定:当图片加载的时候保证图片不会移位。 (当时感觉老牛皮,其实就是让你强制传入width和height实现的😂)
  • 更快的加载页面:该组件仅在进入视窗时才会加载图片,使用是浏览器自带的懒加载策略,并且支持模糊的占位符设置。(确是牛皮,结合OSS相关特性,我们可以实现根据图片的颜色渐变加载图片的功能)
  • 访问策略灵活:按需调整图片大小,根据设备加载最佳的分辨率图片。 (这一点其实得益于html标签功能,程序计算好可能的尺寸,浏览器能自动加载合适的图片尺寸)

自己使用下来确实很方便,解决SSR应用中一个很棘手的问题。来说下自己使用下来的最佳实践(自己是结合阿里云OSS来做的):

  1. 图片资源一定要上OSS,且一定要复写Image组件的loader策略(否则会走Next.js内部处理图片,非常吃cpu)
  2. 可以请后端同学配合返回图片时返回图片的一些信息比如:宽高图片的主色调等。方便前端层面做模糊加载图片效果,防止因为图片宽高自适应,产生页面抖动。

如何提高Next.js应用评分

最后付上一个阿里云OSS图片加载的自定义组件,完整代码请看gist

import Image from "next/image";
import type { ImageProps, ImageLoader } from "next/image";
import { imageFormat } from "src/utils";
import { useCallback } from "react";

export default function ImageAliyun(
  props: Omit<ImageProps, "loader"> & {
    isWebp?: boolean;
    mainColor?: string;
    isBlur?: boolean;
    /**
     * 是否渐进显示
     * @see https://help.aliyun.com/document_detail/44704.html
     *
     * */
    isInterlace?: boolean;
  }
) {
  const {
    isWebp = true,
    quality = 90,
    isBlur = false,
    isInterlace = false,
    mainColor = "237, 237, 237",
    ...rest
  } = props;

  const loader: ImageLoader = useCallback(
    ({ src, width, quality }) => {
      const otherConfig = isWebp ? "/format,webp" : "";
      const interlace = isInterlace ? "/interlace,1" : "";
      return imageFormat(src, width, quality) + otherConfig + interlace;
    },
    [isInterlace, isWebp]
  );

  return rest.src ? (
    <Image
      {...rest}
      quality={quality}
      loader={loader}
      width={props.width}
      height={props.height}
      onError={(e: any) => {
        if (isWebp) {
          e.target.src = rest.src.toString().replace(//format,webp/g, "");
        }
        e.target.onerror = null;
        e.target.style = "filter:none;";
      }}
      alt={props.alt}
      placeholder={isBlur ? "blur" : "empty"}
      blurDataURL={isBlur ? rgbDataURL(mainColor) : undefined}
      ></Image>
  ) : null;
}

// Pixel GIF code adapted from https://stackoverflow.com/a/33919020/266535
const keyStr =
  "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";

const triplet = (e1: number, e2: number, e3: number) =>
  keyStr.charAt(e1 >> 2) +
  keyStr.charAt(((e1 & 3) << 4) | (e2 >> 4)) +
  keyStr.charAt(((e2 & 15) << 2) | (e3 >> 6)) +
  keyStr.charAt(e3 & 63);

const rgbDataURL = (mainColor: string) => {
  const [r, g, b] = mainColor.split(",").map(Number);
  return `data:image/gif;base64,R0lGODlhAQABAPAA${
    triplet(0, r, g) + triplet(b, 255, 255)
  }/yH5BAAAAAAALAAAAAABAAEAAAICRAEAOw==`;
};

其次,说下减少网络字体的闪烁问题,在别一篇文章已经说了在Next.js中如何加载字体的实践。主要使用 nextjs/font这个组件提供的功能。

  1. 使用组件提供的preload 功能,让需要加载字体的页面进行预加载,来减少获取字体的时间
  2. font-display 使您能够通过使用auto、swap、block、fallback和optional值来修改自定义字体的渲染行为。遗憾的是,所有这些值(除 optional 外)都可能通过上述某种方式导致重排。
  3. 要注意一点,并是设计师只是使用了不同字体的文件都直接加载字体,毕竟有些字体文件还是很大的。如果不多的话,建议优先使用svg进行特定字符的渲染。

最后说下,页面动画引起的CLS问题。在使用Lighthout进行检测时,有对应的提示。都是有关于页面的animation相关的

如何提高Next.js应用评分如何提高Next.js应用评分

对于元素动画的设置官方的建议使用css中transform来做动画或者过渡(这也我们在平时开发中需要注意的):

  1. 使用transfrom: scale()来代替和调整使用height或者width属性
  2. 使用transform: translate来替代 topleftrightbottom
LCP(Largest Contentful Paint) 最大内容绘制

如何提高Next.js应用评分

LCP是会根据页面首次开始加载的时间点来报告可视区域内可见的最大图像或文本块完成渲染的相对时间。

哪些元素在是考量的范围:

  1. img 元素
  2. 内嵌在svg元素内的image元素
  3. video元素
  4. 通过url()函数(而非使用css渐变)加载的带有背景图像的元素
  5. 包含文本节点或者其它行内文本元素子元素的块级元素

如果在首屏渲染,加载这些元素所需的时间将对 LCP 产生直接影响。有几种方法可以确保尽快加载这些文件:

  • 优化和压缩图像
  • 预加载重要资源
  • 压缩文本文件
  • 基于网络连接交付不同资产(自适应服务)
  • 使用 Service Worker 缓存资产

说实话这一块指标说的就很抽象,能看懂。按官方给的优化点也做了。评分也正常了,但是你跑的时候还给我有相关提示:

如何提高Next.js应用评分

这是几个意思呢?有知道的大佬能告知下,如何消除这些提示。

除了这两个很重要的指标之外,还有FCPTTFBFIDINP等这些指标。在这里我目前的项目中并没有相关的评分异常。打开web.dev网站了解更详细的知识点吧。

无障碍优化

这一部分,只是单纯的提高的评分。对整体的html结构在无障碍层面的优化。这一部分只要我们在eslint中开启:eslint-plugin-jsx-a11y,一般都没有大问题。下图是自己项目的一些问题,按提示修改即可。

如何提高Next.js应用评分

最后,跑下新评分!

如何提高Next.js应用评分

构建篇

上文中已经分析并解决了,在开发后对于代码层面和Lighthouse部分指标的优化。接下来我说下上线前的整体优化。

缓存

想要提高网站的加载速度,除了直接减少首次加载的资源大小外,还有就是尽可能高的让用户访问时只加载必要的动态资源,且除首次加载外,再次访问的资源都走cache(缓存) ,大大减小加载资源体积。(Use caching wherever possible)

这里主要是用了浏览器缓存这一块的知识,如果不是很清楚可以参考cache-control headers

Next.js是一个服务器渲染的框架,是运行在Node.js环境中,我们可以在程序response时,添加缓存的相关配置。

注:需要注意的是,对于整个Next.js缓存的配置在开发环境下,我们是看不到对应的效果。默认情况下,为了开发方便调试,Next.js会设置cache-control为:no-cache, no-store, max-age=0, must-revalidate。所以要想看到效果,请在next start或者线上环境下查看。

静态资源缓存

程序打包之后在/.next/static目录下,Next.js提供开箱即用的缓存配置,对该目录下的js、css、静态图片和其它媒体资源,默认的缓存配置如下:

# immutable 只能被用在 https://
Cache-Control: public, max-age=31536000, immutable

其中Cache-Control请求头的配置可以被next.config.js中的headers属性复写。如果你想对于某些类型的资源进行设置特别的缓存时间,比如:图片,如果恰巧使用了next/image进行加载图片,你可以设置 minimumCacheTTL,来配置其缓存时长。

getServerSideProps

对于Next.js的SSR应用,基本都要通过该API进行在服务端获取数据进行预渲染。默认情况下输出的Cache-control为:Cache-Control: private, no-cache, no-store, max-age=0, must-revalidate,即不进行缓存。

我可以通过使用 stale-while-revalidate技术,为响应头添加一定时长的缓存(根据页面对数据的实时性进行评估缓存时长),进一步优化页面的响应(官方代码片段)。

export async function getServerSideProps({ req, res }) {
  res.setHeader(
    'Cache-Control',
    'public, s-maxage=10, stale-while-revalidate=59'
  )

  return {
    props: {},
  }
}
getStaticProps

该API一般用于在SSG(页面静态化)渲染中。SSG在构建时有一个比较强制的要求,后端的API一定是能正常响应的,否则会构建失败。一般用在一些静态展示上,比如:关于我们、xxx规则之类页面,可能上线之后很少修改的情况。其中返回值中有一个属性revalidate,用来设置多少秒之后进行重新生成页面。对于使用getStaticProps请求的页面,推荐如下配置。

export async function getStaticProps({ req, res }) {
  res.setHeader(
    'Cache-Control',
    'public, s-maxage=31536000, stale-while-revalidate'
  )
 
  return {
    props: {},
    // Next.js will attempt to re-generate the page:
    // - When a request comes in
    // - At most once every 10 seconds
    revalidate: 10, // In seconds
  }
}

CDN

CDN的好处,在这里就不在多说。SSR类型应用走CDN之后对于整体网站的响应速度提升是巨大的。这里结合Docker构建添加CDN上传为例子(阿里云CDN)

在上代码之前,先说下Docker构建Next.js 12之后的相关优化。在next.config.js下新增了output中的standalone该模式下,Next.js会自动分析运行时必要的node_modules且copy到特定的运行目录。从而大大减小Docker镜像的体积(如果使用了next/image的图像优化要添加必须的依赖,参考这里)。以自己的程序为例子镜像体积从382MB骤减到 53MB左右。

上传CDN这个步骤,一般是结合CD来做,有条件请运维同学配合下即可。上一个没条件的docker多层部署方式:

# Install dependencies only when needed
FROM node:16-alpine AS deps
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories \
&& apk add --no-cache libc6-compat
WORKDIR /app

# Install dependencies based on the preferred package manager
COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* .npmrc ./
RUN \
if [ -f yarn.lock ]; then yarn --frozen-lockfile; \
elif [ -f package-lock.json ]; then npm ci; \
elif [ -f pnpm-lock.yaml ]; then yarn global add pnpm && pnpm i --frozen-lockfile; \
else echo "Lockfile not found." && exit 1; \
fi


# Rebuild the source code only when needed
FROM node:16-alpine AS builder
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories \
&& apk add --no-cache git

WORKDIR /app

# 阿里云 ossutils args
# https://help.aliyun.com/document_detail/50452.html
# ARG ACCESS_KEY_ID
# ARG ACCESS_KEY_SECRET

# 为了更好的缓存,把它放在前边
RUN wget http://gosspublic.alicdn.com/ossutil/1.7.7/ossutil64 -O /usr/local/bin/ossutil \
&& chmod 755 /usr/local/bin/ossutil \
# 自己替换对应的变量,或者运行时通过docker env注入
&& ossutil config -i $ACCESS_KEY_ID -k $ACCESS_KEY_SECRET -e oss-cn-hangzhou.aliyuncs.com


COPY --from=deps /app/node_modules ./node_modules
COPY . .

RUN /app/scripts/build.sh


# Production image, copy all the files and run next
FROM node:16-alpine AS runner
WORKDIR /app

ENV NODE_ENV production
# Uncomment the following line in case you want to disable telemetry during runtime.
# ENV NEXT_TELEMETRY_DISABLED 1

RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs

COPY --from=builder /app/public ./public

# Automatically leverage output traces to reduce image size
# https://nextjs.org/docs/advanced-features/output-file-tracing
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static

USER nextjs

EXPOSE 3000

ENV PORT 3000

CMD ["node", "server.js"]

在package.json中添加对应的script

"scripts": {
	"oss:cli": "ossutil cp -rf --meta Cache-Control:max-age=31536000 ./.next/static oss://cdn.xxx.com/_next/static"
}

同时在next.config.js中根据不同的环境设置下assetPrefix的值。

const isProd = process.env.NODE_ENV === 'production'
const nextConfig = {
	assetPrefix: isProd ? "https://cdn.xxx.com/": undefined,
}

HTTP2

启用HTTP2主要有以下收益:

  1. 多路复用:多个请求使用一个TCP链接,降低连接时间
  2. 二进制格式传输信息:贴合机器语言,更好的性能
  3. 首部压缩:降低头部的体积

以阿里云OSS为例子,参考这里,在CDN设置里启用即可。

如何提高Next.js应用评分

其它

  • DNS 预解析 dns-prefetch <link rel="dns-prefetch" href="//cdn.xxx.com/" />

备注: 如果页面需要建立与许多第三方域的连接,则将它们预先连接会适得其反。preconnect 提示最好仅用于最关键的连接。对于其他的连接,只需使用 即可节省第一步——DNS 查询——的时间。

  • 提前建立网络连接 rel=preconnect
  • 如果当面页面fetch请求后端的接口数据过多,可以在node层进行一次合并进行返回(node层进行合并时,正式环境下可以走K8S中 内网服务 ,非必要不走公网链接) ,用来降低页面的整体请求次数。
  • 支持 HSTS,在http服务启用https协议之后,默认情况下浏览器会经历一次重定向。在配置之后浏览器直接会自动跳转HTTPS

如何提高Next.js应用评分

Polyfills

polyfills这个东西是JS高版本语法在低版本的实现。在之前用webpack开发有一个这样的经验:

  1. 在webpack打包的客户端时候,打包出两个版本的打包 es5(为旧的浏览器准备) 和 es6(为新的浏览器准备)
  2. 使用polyfill.io这类服务通过检测UA动态下发对应的Polyfills
  3. 然后加载对应版本的打包脚本,一定是ES6在前 ES5在后
<!-- dynamic load prolyfill -->
<script src="https://polyfill.io/v3/polyfill.min.js?features=Intl%2Cblissfuljs%2Ces2017%2Ces2019%2Ces2022%2Ces5"></script>
<!-- es6 -->
<script type="module" src="app.js"></script>
<!-- es5 -->
<script defer nomodule src="classic-app-bundle.js"></script>

这样下来,我们就能做到,支持ES Module的浏览器只加载新版本的build脚本,旧版本的进行全量加载,达到最优的方式加载,更多详情可以参考这里和这个链接

现在这一切Next.js已经帮你处理好了,在打包时已经内置了一部分polyfill,且有对应的eslint规则,这些Polyfill只会在旧版本浏览器访问时进行输出。参见 is the anyway to disabled the polyfills

In addition, to reduce bundle size, Next.js will only load these polyfills for browsers that require them. The majority of the web traffic globally will not download these polyfills.

总结

本文通过一个Next.js应用从开发完成到上线,通过@next/bundle-analyzerLighthoutse进行逐步分析优化整个应用的性能。该应用也是一个简单的官网项目,这些手段也只是冰山一脚,自己也在学习中,这是仅提供一个思路,大家可以见招拆招。

另外web.devWeb Almanac都是两个不错的网站,值得深入研究。

参考

  1. almanac.httparchive.org/en/2022/per…
  2. nextjs.org/docs/pages/…
  3. mp.weixin.qq.com/s?__biz=Mzk…
  4. almanac.httparchive.org/zh-CN/2022/
  5. github.com/chen86860/b…
  6. 图片宽高计算器
  7. mp.weixin.qq.com/s?__biz=Mzk…
转载自:https://juejin.cn/post/7250008208160079909
评论
请登录