为了确保用户有良好的首屏加载体验,网站应该努力将核心的页面加载时间(LCP)控制在 2.5秒 之内。然而,在我之前负责的B端项目中,大部分核心页面的LCP时间 高达6秒以上,这直接影响了用户的首屏加载体验。

当然,提高性能有很多方法,但服务器端渲染(SSR)是突破 JavaScript 资源加载和执行瓶颈的关键技术,实现首屏加载时间控制在 2.5 秒以内的效果。
最近我接手了公司的一个新B端项目,本想趁着项目刚起步,尚未积累太多技术债务的时候就优化首屏加载时间。为此,我在项目预研阶段选择了Next.js,考虑到它已经是一个成熟的适用于B端项目的SSR框架。然而,最终我却放弃了使用Next.js。接下来的,我将分享一些我对这一决定的思考。
UI问题
B端项目通常会使用类似于 Ant Design UI 的UI组件库,而我所在的公司前端团队自己实现了一个UI组件库。然而,公司内部的UI组件库不支持直接的服务器端渲染(SSR)调用,主要是因为以下原因:
1.组件外部调用window API
公司内部的UI组件库中的大部分组件需要在组件外部调用window对象的API。例如,假设一个按钮(Button)UI组件的实现中包含以下代码:
js
// 在NextJS中,服务器端在组件外部调用window会报错,因为window是空的
console.log(window.location.pathname);
const Button = () => {
return <button>MyButton</button>;
};
在Next.js中调用这个Button组件会报错,因为Node.js环境中并不存在window对象,因此调用window的API会报错。Button UI组件的需要改成这样才能被NextJS正常调用:
js
import { useEffect } from "react";
const Button = () => {
// NextJS服务端会忽略useEffect里面的代码,因此这样写不会报错
useEffect(() => {
console.log(window.location.pathname);
}, []);
return <button>MyButton</button>;
};
在NextJS的服务端环境中,组件内部的useEffect代码会被忽略,在useEffect里面的window对象不会被调用,因此能正常调用。
2.import 全局css问题
公司内的UI组件库的组件最终的产物里会有import 全局 css 文件的代码,所以需要调用方的构建环境提供编译css的webpack loader:
js
// 最终的Button组件产物里会import css,NextJS编译这个代码会报错
import "./index.css";
const Button = () => {
return <button>MyButton</button>;
};
在NextJS调用这个UI组件会报这个错:
Global CSS cannot be imported from files other than your Custom . Due to the Global nature of stylesheets, and to avoid conflicts, Please move all first-party global CSS imports to pages/_app.js. Or convert the import to Component-Level CSS (CSS Modules).
NextJS为了解决css冲突问题,强制不允许自定义组件import 全局 css。导致引入公司内UI组件库报错。
使用Ant Design UI库?
你可能会问,为什么不直接使用 Ant Design 呢?是的,Ant Design 支持 NextJS,我也是使用 Ant Design 来避开上述问题。但是公司的UI设计师会根据公司的UI组件来设计UI稿。如果使用Ant Design UI库,那么我们需要额外的工作量来调整UI上的差异,以使其与现有UI组件协调一致。
业务组件
B端项目中存在大量重复性较高的功能,因此其他同事通常会将这些通用性较高的功能抽象成业务组件。然而,这些业务组件通常并未考虑到服务器端渲染(SSR)的场景,和UI组件一样在 NextJS 上调用会有两个问题:
- 组件外部调用window API问题
- import 全局 css 问题
业务组件上面两个问题也有解决办法:
业务组件与UI组件的不同之处在于,大部分业务组件并不需要在 SSR 服务中进行渲染,而是可以只在客户端中进行渲染。NextJS 的 dynamic方法 可以做到组件仅在客户端中渲染:
js
"use client";
import dynamic from "next/dynamic";
// 使用dynamic方法,让组件仅在客户端渲染时渲染
const SomeComponent = dynamic(() => import("@scope/some-component"));
const MyApp = () => {
return (
<div>
<SomeComponent />
</div>
);
};
export default MyApp;
使用这个方法可以解决上述的第一个问题:服务端不会调用这些业务组件的window API了。
使用了dynamic后,组件的js和css都会懒加载,也就是说业务组件的css不会在服务端中加载了,因此可以在next.config.js下,放开业务组件的css构建限制:
js
// next.config.js
const nextConfig = {
webpack(config) {
config.module.rules.push({
// NextJS的webpack配置中默认会忽略匹配的less和css。所以要重新自定义less和css的loader配置
test: /(?<!NEXTJS_CSS_DETECTION_FILE)\.(less|css)$/,
include: /node_modules\/@scope/,
exclude: /src/,
use: [
{
loader: "style-loader",
},
{
loader: "css-loader",
options: { modules: false, esModule: false, importLoaders: 1 },
},
{
loader: "postcss-loader",
options: {
postcssOptions: {
config: false,
},
},
},
{
loader: "less-loader",
options: { lessOptions: { javascriptEnabled: true, math: "always" } },
},
],
});
return config;
},
};
module.exports = nextConfig;
虽然解决了业务组件的使用问题,但是dynamic的引入方式还是会有点麻烦,对于新加入进来的开发同学不是很友好。
生态与复用
在我所在的公司部门,前端开发的特点是将多个B端项目分配给不同的团队负责。因此,公司的架构组开发了一个类似于UmiJS的前端框架,以确保统一的代码构建流程,并为大家提供了很多可复用的功能,例如上报系统等。
如果转向使用 Next.js,我们将脱离这个生态,需要自行实现许多可复用的功能,额外增加工作量。
开发难度与开发时间
使用 Next.js 会增加项目的代码复杂性。它要求开发者了解服务端组件和客户端组件之间的区别,同时要求对 window API 的使用方式有一定程度的了解。更重要的是,SSR 会额外增加一个服务,即增加维护服务的成本。许多前端同事在如何保持服务稳定方面的知识和经验仍然相对欠缺。
实际上,上述问题只要有充足的时间都是可以解决的。然而,该项目最紧缺的资源就是时间,项目的开发周期只有一个月。如果选择使用 Next.js,很可能会导致项目延期。
总结
在我所处的公司中使用 Next.js 时所面临的最大问题在于公司内部的生态局限。UI组件库、业务组件库以及前端框架均不支持服务器端渲染(SSR),且前端架构团队的成员以及前端业务开发人员在开发时不会考虑 SSR 的应用场景。仅凭我个人的力量,目前还无法在功能比较复杂的页面上使用 Next.js。如果真的打算推动使用 Next.js,那么必须与前端架构团队共同合作,共建支持服务器端渲染的生态系统。