先说结果:共减少了 580.8 kb 的 First Load JS 大小!

写前端的时候,功能只写了一点,原以为体积很小,部署后发现进入的时间越来越慢,我开始了深深的思考...

最简单的就是使用 Google LightHouse 进行检查,分析告诉我,我加载的 JS 文件太大了,我只好返回去看看编译的时候返回的大小情况

当我运行 next build 的时候,它却告诉我一个十分悲惨的事实:

Route (pages)                              Size     First Load JS
┌ λ /                                      706 B           111 kB
├   /_app                                  0 B             108 kB
├ λ /[pages]                               967 B           691 kB ⚠️
├ λ /404                                   188 B           108 kB
├ λ /category/[slug]                       1.05 kB         133 kB
├ λ /links                                 2.12 kB         559 kB ⚠️
├   └ css/5daf79f472e2d72a.css             1.76 kB
├ λ /posts                                 1.08 kB         133 kB
├ λ /posts/[category]/[slug]               1.13 kB         691 kB ⚠️
└ λ /tag/[name]                            1.01 kB         133 kB
+ First Load JS shared by all              119 kB
  ├ chunks/framework-8792ff04d9bb21fc.js   45.7 kB
  ├ chunks/main-465434c0b66bfe41.js        31.1 kB
  ├ chunks/pages/_app-8f9e59e58813e545.js  29.8 kB
  ├ chunks/webpack-06f027d2973c0918.js     1.01 kB
  └ css/82c644df5d8e11c0.css               11.7 kB

/[pages] 居然达到了惊人的 700 kb。 这都接近 1M 了,再加上我的服务器在海外,加载速度能不慢吗?

使用 Analyze 分析问题所在

为了发现问题所在,我只能安装 @next/analyze 去分析到底是哪些文件这么丧心病狂

// in cmd
pnpm i -D @next/bundle-analyzer

// in next.config.js
if (process.env.ANALYZE === 'true') {
  plugins.push([require('@next/bundle-analyzer')({ enabled: true })])
}

接着运行起 ANALYZE=true next build ,等一会儿就能在浏览器看到具体情况了

analyze ui

👆这张图片是后来截的图了,之前的忘记截了

将鼠标放置上去即可看到这个文件的 Size,首先先优化一下 /_app

移除/合并多余的组件

_app.tsx 中我的 Layout 是这么写的:

import Header from '../components/layouts/Header'
import Footer from '../components/layouts/Footer'
import MainLayout from '../components/layouts'

function App({ initialData, Component, pageProps }) {
  return (
  	<>
    	<Header />
    	<main>
      	<div className="wrap min">
  	      <Component {...pageProps} />
      	</div>
    	</main>
   	 	<Footer />
    </>
  )
}

很明显,写在 App 中的代码非常多余,Header, Footer 这些根本就没有需要 Props 的就不应该出现在这个地方

我主要变化的组件只有 <Component />。应该将那些组件放到一个文件里面实现。

除了这个还有一种情况: 使用了第三方的 layout 组件

换句话说,就是建议你自己实现它,而不要使用第三方组件,第三方组件为了兼容各方,会写入许多你获取用不上的内容(确信)

In AppLayout.tsx

export const AppLayout: FC<React.PropsWithChildren> = ({ children }) => {
  // ...
  return (
  	<>
    	<Header />
    	<main>
      	<div className="wrap min">
			{children}
      	</div>
    	</main>
   	 	<Footer />
    </>
  )
}

// const Header: FC....
// ...

In _app.tsx

import AppLayout from '../components/layouts/AppLayout'

function App({ initialData, Component, pageProps }) {
  //...
  return (
    <AppLayout>
      <Component {...pageProps} />
    </AppLayout>
  )
}

动态导入组件

动态导入使用的是 nextjs 中自带的模块,并不需要安装,在文件里面引入即可。我那500多KB的减少,就是靠的这个模块. 使用 dynamic 导入组件不会包含在页面的 First Load JS 中

毅然选择动态导入你认为会有影响的组件

前天刚给前端添加了一个 Commander 的 Feature,使用的是 cmdk 项目,我自己心知肚明这个是导致 First Load JS 偏大的一个原因,由于考虑到用户一进来并不会立即使用到这个 Commander,于是我打算将它使用 dynamic 动态导入.

// layouts/AppLayout.tsx
// 因为他并不需要 Props,也没有必要展现在 _app 里面,所以也是写在 AppLayout 里的
const Commander = dynamic(() => import("../../widgets/Commander"), {
  ssr: false
})

const AppLayout:.... ({children}) => {
  return (
    <>
      <... />
      <Commander />
    </>
  )
}

就只是换了一种写法,将 import Commander from '...' 变成了 dynamic(() => import("...")),调用它还是一样的。那这里就简单说一下dynamic的第二个参数:

  • suspense: 与 <Suspense fallback={``} /> 同用,页面会先渲染 Suspense fallback,import 的组件会被解析后才进行渲染
  • ssr: 如果 false 就会在客户端动态加载这个组件,如果组件依赖于浏览器 API (window),那你应该把这个设置为 false

官方的解释在这里: https://nextjs.org/docs/advanced-features/dynamic-import

最后在build阶段返回的 First Load JS 大小果然有所改变:108 kB --> 95.5 kB


由于文章与页面的代码相差并不多,那就直接把目光聚集到 [pages]/index.tsx 中,首先要找到到底是什么导致了 First Load JS 这么大的。你问我方法...真就排除法呗 😂

排除后动态导入有可能会影响的组件

import { GetServerSideProps, NextPage } from "next";
import Head from "next/head";
import Link from "next/link";
import { useEffect, useState } from "react";
import { useSnapshot } from "valtio";
import Markdown from "../../components/Markdown";
import SEO from "../../components/others/SEO";
import Comments from "../../components/widgets/Comments";
import appState from "../../states/appState";
import { apiClient } from "../../utils/request.util";
import { isClientSide } from "../../utils/ssr.util";

不知道你是否觉得其中的 Markdown, SEO, Comments,都非常可疑,为了解决我的疑惑,我直接注释掉了其中的一个引入和使用了它的代码。结果非常明显,当我注释了 Comments 后, 678 kB --> 546 kB,于是我立马选择了动态导入。那其实这三个都是带有较大影响的,我就不一一举例了。

最终把这些都进行动态导入后,我得到的结果是这样的:

Route (pages)                              Size     First Load JS
┌ λ /                                      3.61 kB        111 kB --> 99.7 kB
├   /_app                                  0 B            108 kB --> 96.1 kB
├ λ /[pages]                               1.08 kB        691 kB --> 97.2 kB
├ λ /404                                   188 B          108 kB --> 96.3 kB
├ λ /category/[slug]                       774 B          133 kB --> 96.9 kB
├ λ /links                                 746 B          559 kB --> 96.8 kB
├ λ /posts                                 801 B          133 kB --> 96.9 kB
├ λ /posts/[category]/[slug]               1.24 kB        691 kB --> 97.3 kB
└ λ /tag/[name]                            732 B          133 kB --> 96.8 kB

你可以看到,都有不同程度的减少.

什么组件可以动态导入但什么组件不行?

  1. 如果用户不是立马就需要使用/看到的,可以
  2. 如果有影响交互效果的,不可以
  3. 动态导入后,大小不降反升的,肯定不可以啦!
  4. 详情页中输出详情的,可以,但我不太建议