|
| 1 | +title: 一文了解 NextJS 并对性能优化做出最佳实践 |
| 2 | +subtitle: 从`是什么`,`为什么`,`怎么做`来为大家阐述 NextJS 以及如何优化 NextJS 应用体验。 |
| 3 | +cover: https://img12.360buyimg.com/img/s1800x1096_jfs/t1/148662/13/30826/72158/6359f477E3e4a45c5/b775a7bebd4bafdb.png |
| 4 | +category: 经验分享 |
| 5 | +tags: |
| 6 | + - Nextjs |
| 7 | + - 性能优化 |
| 8 | + - 最佳实践 |
| 9 | +author: |
| 10 | + nick: 阿文 |
| 11 | + github_name: AwesomeDevin |
| 12 | +date: 2022-10-27 21:00:00 |
| 13 | +wechat: |
| 14 | + share_cover: https://img12.360buyimg.com/img/s1800x1096_jfs/t1/148662/13/30826/72158/6359f477E3e4a45c5/b775a7bebd4bafdb.png |
| 15 | + share_title: 一文了解 NextJS 并对性能优化做出最佳实践 |
| 16 | + share_desc: 从`是什么`,`为什么`,`怎么做`来为大家阐述 NextJS 以及如何优化 NextJS 应用体验。 |
| 17 | +--- |
| 18 | + |
| 19 | +## 引言 |
| 20 | +从本文中,我将从`是什么`,`为什么`,`怎么做`来为大家阐述 NextJS 以及如何优化 NextJS 应用体验。 |
| 21 | +## 一、NextJS是什么 |
| 22 | +`NextJS`是一款基于 React 进行 web 应用开发的框架,它以极快的应用体验而闻名,内置 Sass、Less、ES 等特性,开箱即用。SSR 只是 NextJS 的一种场景而已,它拥有`4种渲染模式`,我们需要为自己的应用选择正确的渲染模式: |
| 23 | +- **Client Side Rendering (CSR)** |
| 24 | + 客户端渲染,往往是一个 SPA(单页面应用),HTML文件仅包含JS\CSS资源,不涉及页面内容,页面内容需要浏览器解析JS后二次渲染。 |
| 25 | +- **Static Site Generation (SSG)** |
| 26 | + 静态页面生成,对于不需要频繁更新的静态页面内容,适合SSR,不依赖服务端。 |
| 27 | +- **Server Side Rendering (SSR)** |
| 28 | + 服务端渲染,对于需要频繁更新的静态页面内容,更适合使用SSR,依赖服务端。 |
| 29 | +- **IncreIncremental Site Rendering (ISR)** |
| 30 | + 增量静态生成,基于页面内容的缓存机制,仅对未缓存过的静态页面进行生成,依赖服务端。 |
| 31 | + |
| 32 | + |
| 33 | +SSG / ISR 都是非常适合博客类应用的,区别在于`SSG是构建时生成,效率较低,ISR是基于已有的缓存按需生成,效率更高`。 |
| 34 | + |
| 35 | + |
| 36 | + |
| 37 | + |
| 38 | + |
| 39 | +## 二、为什么选 NextJS |
| 40 | +### 优点: |
| 41 | +1. **首屏加载速度快** |
| 42 | + 我们的内嵌场景比较丰富,因此比较`追求页面的一个首屏体验`,NextJS 的产物类似 `MPA(多页面应用)`,在请求页面时会对当前页面的资源进行`按需加载`,而不是去加载整个应用, 相对于 SPA 而言,可以实现更为极致的用户体验。 |
| 43 | +2. **SEO优化好** |
| 44 | + SSR \ SSG \ ISR 支持`页面内容预加载`,提升了搜索引擎的友好性。 |
| 45 | + |
| 46 | +3. **内置特性易用且极致** |
| 47 | + NextJS 内置 `getStaticProps`、`getServerSideProps`、`next/image`、`next/link`、`next/script`等特性,充分利用该框架的这些特性,为你的用户提供更高层次的体验,这些内容后文会细讲。 |
| 48 | + |
| 49 | +### 缺点: |
| 50 | +1. **页面响应相对于SPA而言更慢** |
| 51 | + 由于页面资源分页面按需加载,每次路由发生变化都需要加载新的资源,优化不够好的话,会导致`页面卡顿`。 |
| 52 | +2. **开发体验不够友好** |
| 53 | + 开发环境下 NextJS 根据当前页面按需进行资源实时构建,影响开发及调试体验 |
| 54 | + |
| 55 | +## 三、如何将 NextJS 应用体验提升到极致 |
| 56 | +作为一名开发者,我们追求的不应该是应用能用就好,而是好用,那么如何评价我们的应用是否好用呢? |
| 57 | +- 最直接的方案当然是通过收集用户反馈来评判 |
| 58 | +- 从开发层面,最直观的就是通过`performance`与`lighthouse`来评判 |
| 59 | +### 3.1 优化前 |
| 60 | +>如你所见,由于应用模块的一个复杂性,我们的 NextJS 应用起初性能并不是很好,甚至谈得上是差 |
| 61 | +- FCP: 首次内容绘制时间1.8s |
| 62 | + |
| 63 | +  |
| 64 | + |
| 65 | + |
| 66 | +- lighthouse: 性能评分报告 55分,Time to Interactive(TTI) 可交互时间为 7.3s,通常是发生在页面依赖的资源已经加载完成。 |
| 67 | + |
| 68 | +  |
| 69 | + |
| 70 | +- network: 我们每次进行路由跳转都要按需加载资源,因此我们需要单个页面的 DomContentLoaded 尽可能快以保证页面 Dom 结构的渲染效率。 |
| 71 | + |
| 72 | +  |
| 73 | + |
| 74 | +- 页面构建时间 |
| 75 | + |
| 76 | +  |
| 77 | +> 这些指标都间接反馈出应用的体验问题亟待解决。 |
| 78 | +
|
| 79 | + |
| 80 | + |
| 81 | +### 3.2 优化措施 |
| 82 | +- 优化用户体验 |
| 83 | + - **1. 开启 gzip 压缩** |
| 84 | + 通过 network 可以看到资源实际大小及 http 请求的 size,如果不开启压缩,二者基本是没有差异的。 |
| 85 | +  |
| 86 | + gzip 优化后可以看到, 压缩效果还是很明显的 |
| 87 | + |
| 88 | + 开启 nginx 的 gzip 压缩 |
| 89 | + ```shell |
| 90 | + gzip on; |
| 91 | + gzip_min_length 100; |
| 92 | + gzip_buffers 4 16k; |
| 93 | + # gzip_http_version 1.0; |
| 94 | + gzip_comp_level 9; |
| 95 | + gzip_types gzip_types text/plain application/x-javascript text/css application/xml text/javascript application/x-httpd-php image/jpeg image/gif image/png application/javascript; |
| 96 | + gzip_vary on; |
| 97 | + gzip_proxied any; |
| 98 | + ``` |
| 99 | + |
| 100 | + 通过 response header 判断压缩是否生效 |
| 101 | +  |
| 102 | + - **2. 针对非首屏组件基于 dynamic 动态加载** |
| 103 | + 在页面加载过程中,针对一些不可见组件,我们应该动态导入,而不是正常导入,`确保只有需要该组件的场景下,才 fetch 对应资源`, 通过 `next/dynamic`,在构建时,框架层面会帮我们进行分包 |
| 104 | + ```javascript |
| 105 | + import dynamic from 'next/dynamic' |
| 106 | + const Modal = dynamic(() => import('../components/mModal')); |
| 107 | + export default function Index() { |
| 108 | + return ( |
| 109 | + {showModal && <Modal />} |
| 110 | + ) |
| 111 | + } |
| 112 | + ``` |
| 113 | + 打开Network。当条件满足时,你将看到一个新的网络请求被发出来获取动态组件(单击按钮打开一个模态)。 |
| 114 | + - **3 . next/script 优化 script 加载时** |
| 115 | + `next/script` 可以帮助我们来`决定 js 脚本加载的时机` |
| 116 | + |
| 117 | + strategy | 描述 |
| 118 | + ---- | --- |
| 119 | + beforeInteractive | 可交互前加载脚本 |
| 120 | + afterInteractive | 可交互后加载脚本 |
| 121 | + lazyOnload | 浏览器空闲时加载脚本 |
| 122 | + |
| 123 | + ```html |
| 124 | + <Script strategy="lazyOnload" src="//wl.jd.com/boomerang.min.js" /> |
| 125 | + ``` |
| 126 | + - **4. next/image 优化图片资源** |
| 127 | + `next/image` 可帮助我们`对图片进行压缩(尺寸 or 质量),且支持图片懒加载`,默认 loader 依赖 nextjs 内置服务,也可以通过`{loader: custom}`自定义loader |
| 128 | + ```js |
| 129 | + import Image from 'next/image' |
| 130 | + const myLoader = ({ src, width, quality }) => { |
| 131 | + return `https://example.com/${src}?w=${width}&q=${quality || 75}` |
| 132 | + } |
| 133 | + const MyImage = (props) => { |
| 134 | + return ( |
| 135 | + <Image |
| 136 | + loader={myLoader} |
| 137 | + src="me.png" |
| 138 | + alt="Picture of the author" |
| 139 | + width={500} |
| 140 | + height={500} |
| 141 | + /> |
| 142 | + ) |
| 143 | + } |
| 144 | + ``` |
| 145 | + - **5. next/link 预加载** |
| 146 | + 基于 `hover 识别用户意图`,当用户 hover 到 Link 标签时,`对即将跳转的页面资源进行预加载`,进一步防止页面卡顿 |
| 147 | + ```js |
| 148 | + import Link from 'next/link' |
| 149 | + <Link prefetch={false} href={href}>目标页面</Link> |
| 150 | + ``` |
| 151 | + - **6. 静态内容预加载** |
| 152 | + 基于 `getStaticProps` 对不需要权限的内容进行预加载,它将在 NextJS 构建时被编译到页面中,`减少了 http 请求数量` |
| 153 | + ```js |
| 154 | + function Blog({ posts }) { |
| 155 | + return ( |
| 156 | + <ul> |
| 157 | + {posts.map((post) => ( |
| 158 | + <li>{post.title}</li> |
| 159 | + ))} |
| 160 | + </ul> |
| 161 | + ) |
| 162 | + } |
| 163 | + export async function getStaticProps() { |
| 164 | + const res = await fetch('https://.../posts') |
| 165 | + const posts = await res.json() |
| 166 | +
|
| 167 | + return { |
| 168 | + props: { |
| 169 | + posts, |
| 170 | + }, |
| 171 | + } |
| 172 | + } |
| 173 | + export default Blog |
| 174 | + ``` |
| 175 | + - **7. 第三方 library 过大时,基于 umd 按需加载** |
| 176 | + 当`第三方 library 过大时,以 umd 进行引入`,在需要的场景下通过 script 进行加载。 |
| 177 | + ```js |
| 178 | + // 封装记载umd模块的hoc |
| 179 | + function loadUmdHoc(Comp: (props) => JSX.Element, src: string) { |
| 180 | + return function Hoc(props) { |
| 181 | + const [isLoaded, setLoaded] = useState( |
| 182 | + !!Array.from(document.body.getElementsByTagName('script')).filter( |
| 183 | + (item) => item.src.match(src) |
| 184 | + ).length |
| 185 | + ) |
| 186 | + useEffect(() => { |
| 187 | + if (isLoaded) return |
| 188 | + const script = document.createElement('script') |
| 189 | + script.src = src |
| 190 | + script.onload = () => { |
| 191 | + setLoaded(true) |
| 192 | + } |
| 193 | + document.body.append(script) |
| 194 | + }, []) |
| 195 | +
|
| 196 | + if (isLoaded) { |
| 197 | + return <Comp {...props} /> |
| 198 | + } |
| 199 | + return <></> |
| 200 | + } |
| 201 | + } |
| 202 | + |
| 203 | + function Upload(){ |
| 204 | + // todo 使用umd模块 |
| 205 | + return <></> |
| 206 | + } |
| 207 | + |
| 208 | + // 使用该组件时,加载hoc |
| 209 | + export default loadUmdHoc( |
| 210 | + Upload, |
| 211 | + 'xxx.umd.js' |
| 212 | + ) |
| 213 | + ``` |
| 214 | + |
| 215 | +- 优化研发体验 |
| 216 | + - **1. 基于 urlimport 进行瘦身,提升编译效率** |
| 217 | + `urlImport` 是 NextJS 提供的一个实验特性,支持加载远程 esmodule |
| 218 | +  |
| 219 | +NextJS 会在本地对所加载的远程模块进行缓存, 减少了我们所需构建的模块数,缺点是它会`影响 treeShaking` 的一个效果,因此在生产环境,建议通过`NormalModuleReplacementPlugin`对 urlimport 的依赖进行一个本地替换 |
| 220 | +  |
| 221 | + |
| 222 | + - **2. webpack 配置选择性忽略** |
| 223 | + 针对一些生成环境的配置我们可以`通过区分环境来进行选择性忽略部分配置`,如 module federation exposes 在开发环境我们就可以忽略掉。 |
| 224 | + |
| 225 | + **dev.conf.js** |
| 226 | +  |
| 227 | + **pro.conf.js** |
| 228 | +  |
| 229 | + - **3. 开启 SWC 编译** |
| 230 | + SWC 是基于 Rust 实现的一款开发工具,既可用于编译也可用于打包,据官方言,它比 Babel 快了 20~70倍,NextJS 在 12 版本默认打开了 SWC 的支持。开启 SWC 后,应用的编译速度将比 Babel 快 17 倍,刷新速度快 5 倍。需要注意的是如果你通过`.babelrc`自定义 babel 配置,SWC 的一些特性将会被关闭。 |
| 231 | + |
| 232 | +### 3.3 优化后 |
| 233 | +从以下指标可以看出我们应用的体验得到了很大提升, 实际的一个交互体验也好了不少,在路由跳转上实现了类似 SPA 的一个体验,即使是各页面资源按需加载不会再出现页面卡顿的情况。 |
| 234 | + |
| 235 | +- FCP: 首次内容绘制时间 从 1.8s 优化到 0.35s,`提升了近 80%` |
| 236 | + |
| 237 | +  |
| 238 | + |
| 239 | +- lighthouse: 评分从55提升到了80,TTI 从7.3s 优化到了 2.4s, `分别提升了 30% / 64%,chrome 的最佳实践分达到了满分`💯 |
| 240 | + |
| 241 | +  |
| 242 | + |
| 243 | +- network: DomContentLoaded 从 2.42s 优化到 0.67s,`提升了 77% ` |
| 244 | + |
| 245 | +  |
| 246 | + |
| 247 | +- 页面构建时间: 基本满足了毫秒级实现页面编译的需求,`提升了 70% 以上` |
| 248 | + |
| 249 | +  |
| 250 | + |
| 251 | + |
| 252 | + |
| 253 | +## 四、后续规划 |
| 254 | +为了实现更为极致的用户体验,我们后续计划将资源上CDN,减少`Waiting for server response`的性能损耗,并加入`PWA`的离线缓存特性。 |
| 255 | + |
| 256 | + |
| 257 | +参考文章 |
| 258 | +[Optimize Next.js App Bundle and Improve Its Performance](https://www.syncfusion.com/blogs/post/optimize-next-js-app-bundle-improve-its-performance.aspx) |
| 259 | +[我看Next.js:一个更现代的海王](https://baijiahao.baidu.com/s?id=1715929965351295334&wfr=spider&for=pc) |
| 260 | + |
0 commit comments