一个网页的完整一生:从敲网址到页面显示,中间发生了什么

这些年一直在写业务代码,对"网页到底是怎么出现在屏幕上的"只有一个模糊印象。最近从零学 Next.js,用 6 天时间把这个问题亲手验证了一遍。这篇文章记录过程和实验数据------不是抄文档,每一步都有我可复现的证据。

文章按一次真实访问的顺序走:页面是什么时候做出来的 → 服务器发来的是哪几样东西 → 点链接为什么不用整页刷新。

第一幕:页面是提前"做"好的

以前我以为网页是"访问时才生成的"------敲下网址,服务器现算一份 HTML 发回来。实际在 Next.js 里,大多数页面是提前"预制"好的:构建时就把 HTML 生成好存着,用户来了直接给现成的,打开飞快。前提是这页内容不因人而异。

我动手做的第一实验:通过读取cookie信息,在页面上显示当前访客的"主题偏好",我在页面里加了一行读 cookie 的代码,然后 build。

结果不是编译通过,而是直接报错退出,原文如下:

vbnet 复制代码
Error: Route "/": Next.js encountered uncached or runtime data during prerendering.

Ways to fix this:
  - [stream] Provide a placeholder with <Suspense fallback={...}> around the data access
  - [cache] For uncached data: cache the access with "use cache"
  - [block] Set export const instant = false to allow a blocking route

原因一想就通:预制的时候根本没有访客,cookie 从哪读?这页内容因人而异,没法预制了。

有意思的是,报错没替我做决定,而是给了三条路,每条都标了名字和做法:

报错给的路 一句话做法 代价
stream(流式) 页面架子照常预制好,cookie 那块留个"加载中"占位,数据到了再补进去 那一小块晚到一会儿
cache(缓存) 读一次存一份,一段时间内后面所有访客直接用存的 数据有一点点旧
block(阻塞) 放弃预制,每个访客来了现做 最慢,服务器开销最大

每条路具体怎么写,也就几行代码的事:

stream(流式)------把读 cookie 的那部分包进 Suspense,给它一个加载中的占位:

tsx 复制代码
<Suspense fallback={<p>加载中...</p>}>
  <ThemeLabel />   {/* 这个组件里读 cookie */}
</Suspense>

cache(缓存)------在读取数据的函数顶部加一行指令,告诉 Next 这结果可以存:

tsx 复制代码
'use cache'

block(阻塞)------页面文件顶部导出一个常量,宣布"这页我放弃预制":

tsx 复制代码
export const instant = false

三条路都不难写,就是选一个:想快一点的用流式,想新一点的用缓存,懒得折腾的直接阻塞。

第一次碰到框架用报错逼你做性能决策,不选清楚不让过。

第二幕:服务器发来的不是"一个网页",是三样东西

打开一个网页,服务器实际发来三份东西:

  • HTML(约 4.3KB):页面本体,你看得见
  • RSC payload(约 4KB):一份"组装说明书"。HTML 只记录页面长什么样;这份说明书记录"页面由哪些组件组成、每个组件的代码去哪个文件拿",藏在 HTML 里一起送到,给浏览器里的 React 用来组装页面
  • JS chunks(约 600KB):交互代码。浏览器先拿到 HTML 把页面显示出来,然后才发现"还需要这些 JS",再回头去下载------所以它们总是最后到。下完之前,页面看得见、点不动。

为了验证"哪些代码真的发货了",我做了标记实验:在服务端组件和客户端组件里各写一句标记,然后分别在三份产物里搜。

先解释这两个词:服务端组件 = 只在服务器上运行的组件,默认就是这种;客户端组件 = 要在用户浏览器里运行的组件,文件顶部标一行 'use client' 才是。

标记写在哪 HTML 说明书 JS chunks
服务端组件 ✅ ✅ ❌ 搜不到
客户端组件 ✅ ❌ ✅

结论很干脆:服务端组件的代码一行都不进浏览器------这就是 RSC 省体积的机制本体。客户端组件的代码必须发货,因为它要在浏览器里执行(这个过程叫"水合":JS 加载完接管页面,按钮才开始响应点击)。

第二幕·续:亲手把代码搬出零件箱

知道"服务端组件代码不发货"之后,我立刻想:demo 里有没有"不用电却跟着发货"的代码?

还真有。我的聊天窗口整个标了 'use client'(它需要 state 管理消息列表,必须通电),但里面住着一堆不需要通电的东西:页面标题、布局外壳。它们跟着零件箱白白发货。

怎么判断哪些能搬?我总结了三问:

  1. 有 state / hooks 吗?
  2. 有事件吗(onClick、onChange)?
  3. 有浏览器 API 吗(window、document)?

三个都没有 → 可以下沉;有任何一个 → 动不了。

具体做法,就是把这段代码从要打包的文件搬到不打包的文件:

tsx 复制代码
// 搬之前:标题和外壳住在 ChatWindow.tsx(要通电 → 代码发货)
return (
  <div className="flex flex-col h-screen ...">
    <h1>AI Chat Demo</h1>
    ...

// 搬之后:它们住进了 page.tsx(服务端组件,不打包)
// ChatWindow 只留消息列表和输入框
return (
  <>
    ...

验证搬家成功,我用四个检查(缺一个都可能把"改坏"当"成功"):

perl 复制代码
① build 后页面还是 ○(静态性没丢)
② grep 零件包:搜不到标题了(不发货了)
③ grep page.tsx:标题还在项目里(区分"搬走"和"删了"的关键)
④ 打开页面:标题在、布局没塌

结果:零件包 612K → 604K。

老实说 8K 在这个小 demo 里微不足道------这个练习的价值不在省下的体积,在于跑通了"发现 → 搬走 → 验证"的完整流程。真实项目里,被误标 'use client' 的静态内容往往几十 KB 起步。

第三幕:点链接切页面,为什么这么快

传统网页点链接 = 整页刷新,白屏、全部重新下载。

Next.js 的做法是提前下载:链接一进屏幕,浏览器就在后台悄悄把目标页面的数据拉下来了,用户毫无感知。这个行为叫预取(prefetch)。

我开了 Network 面板实测:

  1. 链接出现在屏幕上(我什么都没点),浏览器就悄悄发出了对该页面的预取请求
  2. 预取还分段:一个请求拿页面外壳,一个拿页面内容(请求头 Next-Router-Segment-Prefetch 里能看到这两个名字)
  3. 我点击链接那一刻,新增网络请求数:0------拿缓存的数据,局部替换页面

还挖到一个文档和现实的差距:官方说"50 个商品链接共享页面外壳,只下载一次",但我在 16.4 上实测同屏两个链接,外壳照样各下载一次。用 curl 重放请求做 diff 后才发现:外壳响应只是一副"骨架"(几百字节的结构清单),真正的内容按内容指纹复用。请求次数不等于传输量------文档和实测各对了一半。

相关推荐
春涧草茶1 小时前
慢就是快16-1协程
开发语言·前端·python
Oiiouui2 小时前
linux服务器部署Logto后端与对应前端的实现
linux·服务器·前端·后端
前端小D2 小时前
JS核心底层
前端·javascript
律宏阔2 小时前
Taro 4 项目从 npm 迁移到 pnpm 踩坑实录
前端·微信小程序·taro
用户30816998764852 小时前
同一句话问 AI 15 次,答案 15 个:temperature 0 实测也不稳
前端
SameX2 小时前
ArkTS 做 5 态数据流 —— 咖啡电量的代谢状态机怎么驱动 UI
前端
Dovis(誓平步青云)2 小时前
启元 T1 Pro 人机相声搭档:语音-触摸双通道的启元机器人节奏引擎设计
开发语言·前端·javascript·microsoft·交互
爱吃烧烤的技术渣2 小时前
别再手写 CRUD 了:我把 Element Plus 的工作量压缩成一份列 schema
前端·vue.js
MetWeave2 小时前
风的三十年:一个机场的风在变小吗:10,958 天的账(《风不是一个数》系列第 6 篇)
前端·数据可视化