京程一灯宣传官网面试手册
# 京程一灯宣传官网面试手册
项目口述 技术原理 高频追问 难点复盘 回答脚本
| 项目 | 内容 |
|---|---|
| 项目定位 | 面向 AI 全栈课程品牌的高沉浸式三语官网与课程转化站点 |
| 核心亮点 | React 19 SPA、WebGPU FFT 动态海洋、Babylon.js 粒子场、Cesium 交互地球、三语国际化、响应式与渐进降级 |
| 交付形态 | 无后端、无运行时密钥的静态站点,目标平台为 Cloudflare Pages |
| 事实基线 | 以 yd-website 当前源码、依赖锁、测试、QA 记录和构建产物为依据 |
| 使用原则 | 先讲业务价值,再讲技术方案;先说结论,再讲原因、项目落点和边界 |
2026 年 9 月
# 阅读导航
这份手册不是让你背一串库名,而是帮助你把“为什么做、怎样拆、哪里难、怎么验证、还有什么不足”连成一条完整叙事。面试官通常不会满足于“我用了 React 和 Babylon.js”,而会继续追问为什么选、失败时怎样处理、性能如何控制、代码是否真的由你完成。
# 按准备时间阅读
| 可用时间 | 优先阅读 | 目标 |
|---|---|---|
| 20 分钟 | 项目介绍、事实边界、三个核心难点、速记卡 | 能把项目讲清楚,不夸大 |
| 半天 | 架构、React、路由、国际化、WebGPU 海洋、Cesium、性能与测试 | 能承受一轮技术深挖 |
| 两天 | 全部技术栈、问题库、压力追问和模拟面试 | 能按面试官方向灵活展开 |
| 一周 | 对照证据索引重新阅读代码,并口述录音 | 把参考答案变成自己的真实表达 |
# 回答的通用结构
- 结论:先用一句话直接回答问题。
- 原因:说明当时的业务目标、浏览器约束或性能约束。
- 项目落点:指出具体模块、调用链或失败路径。
- 验证:说明测试、浏览器检查或构建约束。
- 边界:主动说没有做到什么,以及下一步怎样改。
# 一 项目介绍与简历表达
# 一句话版本
这是一个面向 AI 全栈课程展示和招生转化的三语沉浸式官网,我用 React 19 和 Vite 搭建静态 SPA,并把 WebGPU FFT 海洋、Babylon.js 粒子场、Cesium 交互地球、国际化、响应式和浏览器自动化测试整合成可以渐进降级的完整交付。
# 三十秒版本
这个项目不是普通模板站,而是一个强调视觉体验、全球化内容和真实部署约束的课程品牌官网。它有首页、课程大纲、公开课、招生和关于五条路由,支持中文、英文、日文。最复杂的部分是首页的实时 FFT 海洋:它依赖 WebGPU 和旧版 Babylon 运行时,所以我把它隔离到 iframe,主站继续使用 Babylon 9,并设计静态预览、超时等待、真正海面首帧检测和失败重试。站内还有 Babylon 粒子技术图谱、Cesium 全球航线、GSAP 滚动动效和 Playwright 三语回归。最终以静态资产部署到 Cloudflare Pages。
# 一分钟版本
项目目标是把课程内容、品牌故事和招生入口做成一套视觉辨识度高、移动端可用、三种语言一致的官网。我把页面层设计为 React 组件,使用轻量的 History API 路由维护五个页面,没有为了五条固定路由引入更重的路由层。国际化由 URL 查询参数、localStorage 和 useSyncExternalStore 组成:URL 便于分享并且优先级最高,本地存储记住偏好,无存储权限时仍退回中文。
视觉层分成三种渲染任务。首页海洋使用独立的 Babylon 5.x WebGPU OceanDemo,计算着色器在频域生成波谱,再通过二维逆 FFT 得到位移和法线;主应用通过 iframe 隔离它与 Babylon 9 的运行时。课程图谱用 Babylon 9 的粒子系统;全球航线用 Cesium,但只在进入视口后动态加载,并采用按需渲染和 45 帧上限。所有重场景都有静态预览、错误状态、重试、prefers-reduced-motion 和资源清理。工程侧配置了 WGSL 原始文本插件、Cesium 静态资源复制、Cloudflare SPA 回退、产物数量与单文件大小检查,以及覆盖三种语言、两种视口、导航、刷新和异常存储的 Playwright 测试。
# 三分钟版本
我先从业务拆分讲。站点服务三类目标:让访客快速理解课程定位、通过交互内容建立品牌记忆、最后进入招生咨询。因此不是所有区域都应该做成 3D。我把首屏海洋、技术能力图谱和全球航线做成三个独立的高体验模块,其余信息仍然使用语义化 HTML、真实文本和常规卡片,保证内容可读、可响应和可降级。
应用入口使用 React 19 createRoot 和 StrictMode。路由层维护五条固定路径,拦截同源内部链接,使用 history.pushState 更新地址,并监听 popstate 支持前进后退,同时迁移旧的 #/path 书签。这里没有服务端路由,所以部署端必须把未知路径回退到 index.html。国际化以中文源文案作为 Key,英文和日文放在 JSON,复杂课程数据递归本地化;语言优先级是合法 URL 参数、已保存偏好、中文默认值。语言状态放在模块级外部 Store,再由 useSyncExternalStore 接入 React,避免每个页面各维护一份状态。
最复杂的是实时海洋。上游 OceanDemo 基于 Babylon 5.x 和 WebGPU 计算着色器,而主站的粒子场使用 Babylon 9。直接混在同一个 JavaScript 上下文里会增加版本、着色器缓存和全局对象冲突风险,所以我保留上游构建为独立页面,用 iframe 做运行时隔离。React 侧不能把 iframe 的 load 当成海面可用,因为 HTML 加载完成后 WGSL 编译和纹理准备仍在继续。我轮询场景实例,检查计算着色器能力,再通过 onAfterRenderObservable 观察实际海洋网格产生有效 draw call 后才触发完成;30 秒只是展示静态预览,不会错误地中止仍在编译的海洋。完全不支持 WebGPU或初始化失败时回退到海景图,并给用户重试入口。
海洋内部把初始波谱、时间演化、二维逆 FFT、纹理合并、分块海面几何、PBR 材质和浮力连接起来。为了把小船融入首页,我复用原浮标已经计算好的波浪位姿,加载压缩后的 GLB 船模,把船挂到独立 carrier 上,并用四元数球面插值压低横摇与纵摇,减少浪峰穿过甲板。海天线不是写死一个视觉猜测,而是把海平面远点经过相机视图投影到屏幕坐标,再写入 CSS 变量,使上层暖色氛围与真实 3D 地平线对齐。
另一个重模块是 Cesium 地球。我没有把完整 Cesium 打进首屏主包,而是在组件进入视口后加载本地 Cesium.js 和样式,并通过 Vite 插件复制 Workers、Assets、Widgets 和 ThirdParty。场景关闭默认控件和连续默认渲染,使用 requestRenderMode、自定义 45 帧循环、页面隐藏暂停和 IntersectionObserver 可见性门控。航线沿椭球测地线插值,再叠加正弦高度形成弧线。组件卸载时取消动画帧、断开 Observer、移除事件并销毁 Viewer。
最后是工程验收。Playwright 测试矩阵覆盖五个页面、三种语言和 1440/390 两种宽度,并检查 html lang、标题、中文残留、横向溢出和页面错误,还单测语言切换后导航、刷新、历史返回、移动菜单、海报同步和 localStorage 不可用。Cloudflare 构建检查会限制单文件不超过 25 MiB、总文件不超过 20000,并验证关键海洋、Cesium 和三语课程资产存在。当前项目仍有优化空间:主 JavaScript 产物较大,静态 3D 资产总体积高,运行时 SEO 不是三语静态预渲染,而且现在是展示型前端,没有后端、数据库和真实 AI 推理链路。我会把这些边界主动说清楚。
# 简历项目描述参考
以下内容应根据你本人真实职责删改,不要把参考答案直接当成个人经历。
- 负责 AI 全栈课程品牌官网从信息架构、React 组件、三语内容到 Cloudflare Pages 交付,完成首页、课程、公开课、招生与品牌五条业务路径。
- 集成 WebGPU 计算着色器与二维逆 FFT 动态海洋,使用 iframe 隔离 Babylon.js 5.x 与主站 9.x 运行时,设计真实首帧检测、静态预览、失败回退与重试机制。
- 使用 Babylon.js 实现可交互技术粒子场,使用 Cesium 实现椭球测地线全球航线,并通过视口懒加载、按需渲染、帧率上限和完整资源清理控制 GPU 成本。
- 设计 URL 与 localStorage 协同的中英日国际化状态,覆盖导航、历史、刷新、弹窗资源和无存储环境;通过 Playwright 建立 34 个浏览器用例的测试矩阵。
- 配置 Vite 的 WGSL 源码加载、Cesium 运行时资产复制与 Cloudflare SPA 回退,增加文件数、25 MiB 单文件限制和关键资产完整性检查。
# 事实边界
| 可以明确说 | 需要限定 | 不要声称 |
|---|---|---|
| 完成 React 静态站、五条路由、三语、3D 场景、响应式和浏览器测试实现 | 目标部署平台是 Cloudflare Pages;仓库有部署说明和产物检查,但域名接入仍受 GoDaddy 账号问题影响 | 已有大量真实用户、已经形成业务转化数据、经过真实流量压测 |
| WebGPU 海洋基于 FFT Ocean 上游实现并做了站点集成与改造 | 海洋算法不是从数学公式开始全部原创;应说明基于开源 OceanDemo,自己的工作是整合、隔离、体验与性能改造 | 独立发明 FFT 海洋算法、完全自研 Babylon 引擎 |
| 主站使用 Babylon 9,隔离海洋运行时使用 Babylon 5.x | 当前仓库同时保留源码、vendor 源和编译后的海洋资产,主站实际通过 iframe 加载预构建页面 | 所有海洋源码都由当前 Vite 主构建实时编译 |
| 有 AI 课程内容和 AI 全栈工程交付背景 | 当前站点本身没有模型 API、RAG、Agent、向量库或后端推理 | 这是一个完整 AI Agent 产品或具备线上大模型推理 |
| 有测试代码和历史 QA 记录 | 当前环境的可选原生依赖安装不完整时,Vite、Oxlint 和 Playwright 启动会失败,需要重新安装依赖后再验证 | 当前所有测试刚刚重新跑过并全部通过,如果你没有真实执行证据 |
最重要的诚实口径:这个项目能证明你具备复杂前端、图形渲染、浏览器兼容、工程化测试与静态部署能力;它不能单独证明后端数据库、分布式系统或大模型应用链路能力。AI 全栈面试时,应把它作为“体验层与交付层项目”,再用其他项目证明 AI 与后端能力。
# 二 系统架构与核心调用链
# 架构总览
浏览器
├─ React 19 应用
│ ├─ 手写 History API Router
│ ├─ URL + localStorage + useSyncExternalStore 国际化
│ ├─ 五个页面与共享 Header / Footer
│ ├─ GSAP / Motion / React Bits 动效
│ └─ HTML / CSS / 图片 / 字体 / 海报 / PDF
├─ Babylon.js 9
│ └─ 课程大纲技术图标粒子场
├─ Cesium 1.145 独立运行时
│ └─ 地球、测地线航线、点、标签与交互
└─ iframe 运行时隔离
└─ Babylon.js 5.x OceanDemo
├─ WebGPU Compute Shader
├─ 初始波谱与时间演化
├─ 二维 IFFT
├─ 海面几何与 PBR 材质
└─ 浮力、小船、天空与反射
构建与发布
Vite 8 → dist 静态资产 → 产物约束检查 → Cloudflare Pages
└─ /* → /index.html 200
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 页面结构
| 路径 | 页面职责 | 代表性技术 |
|---|---|---|
/ | 品牌首屏、课程价值、能力和转化入口 | WebGPU 海洋、Canvas Loader、响应式首屏 |
/curriculum | 技术课程图谱与漫画大纲 | Babylon.js 粒子、海报弹窗、下载与语言同步 |
/open-class | 公开课、AI 发展史与全球视野 | 翻页书、Liquid Glass 卡片、Cesium 地球 |
/admissions | 招生信息、优势、方向、流程与咨询 | 2.5D 图标、动态光场、交互 CTA |
/about | 品牌介绍、里程碑和价值观 | 卡片聚光、滚动进入动画 |
# 一次页面导航发生了什么
- 用户点击站内链接。
Router的文档级 click 监听只处理左键、未按修饰键、同源、非下载、非新窗口且属于白名单的链接。localizedPath自动保留或补上当前lang参数。history.pushState改变 URL,再主动派发popstate。- 路由状态更新,页面组件切换,滚动位置回到顶部。
pageMetadata根据当前页面和语言更新<html lang>、document.title和 meta description。- 浏览器前进后退会触发真实
popstate,旧的#/path地址会被replaceState迁移为干净路径。
# 一次语言切换发生了什么
- 首次加载先读 URL 的
lang;合法值为zh、ja、en。 - URL 没有合法语言时尝试读取 localStorage;访问被禁用或抛错时忽略。
- 两者都没有时使用中文。
setLocale用replaceState写回当前 URL,并尝试保存偏好。- 模块级 Store 通知所有订阅者,React 通过
useSyncExternalStore重新取快照。 - 普通文案通过
translate查 JSON;数组和普通对象由translateData递归处理;React 图标对象保持引用不变。 - 新导航继续携带语言,刷新和历史返回仍能恢复。
# 动态海洋启动链路
React 首屏挂载
↓
始终先准备静态海景图
↓ navigator.gpu 是否存在
不支持 ─────────────→ 直接静态预览 + 不支持提示
支持
↓
加载 /ocean-demo/index.html iframe
↓
OceanDemo 创建 WebGPUEngine、编译 WGSL、加载纹理和模型
↓
父页面轮询 iframe.scene 并检查 compute shader capability
↓
设置固定相机、天空、海天线同步、加载船模
↓
观察真实海面网格的有效 draw call
↓
双 requestAnimationFrame 等浏览器合成
↓
隐藏 Loader,显示实时海洋
30 秒仍未完成:显示静态预览,但后台继续编译
初始化失败:静态预览 + 失败提示 + 重试按钮
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# Cesium 地球启动链路
CareerGlobe 挂载
↓ IntersectionObserver
进入视口附近才 start
↓
动态插入本地 Cesium.js 与 widgets.css
↓
创建 Viewer,关闭无关控件和默认连续循环
↓
加载日夜单图纹理、经纬网、点、标签和测地线航线
↓
等待 tilesLoaded 且屏幕中心确实可拾取到地球表面
↓
静态预览淡出,交互控件启用
↓
可见时自定义 45 FPS 更新航线头部和相机
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 三 技术栈全景
| 技术 | 当前版本或形态 | 项目职责 | 面试重点 |
|---|---|---|---|
| JavaScript / JSX | ES Module | 页面、交互和状态主语言 | 闭包、事件、异步、清理、模块边界 |
| TypeScript | 5.9.3,允许 JS,strict: false | 海洋源码、着色器工具的类型表达 | 渐进迁移与类型边界,不要声称全项目严格 TS |
| React | 19.2.8 | 组件、状态、Effect、外部 Store 接入 | 渲染模型、Effect 清理、StrictMode |
| React DOM | 19.2.8 | createRoot 挂载 | 客户端渲染与 hydration 区别 |
| Vite | 8.2.2 | 开发、资源处理、生产构建 | 插件、静态资源、SPA 与构建边界 |
| Babylon.js | 主站 9.23;隔离海洋 5.x | 粒子场、WebGPU 海洋、PBR、模型与浮力 | WebGL/WebGPU、场景生命周期、版本隔离 |
| WGSL / WebGPU | Compute Shader | 波谱演化和 IFFT | GPU 通用计算、工作组、能力检测 |
| Cesium | 1.145 | 3D 地球、测地线航线、标签与交互 | 椭球坐标、懒加载、按需渲染 |
| GSAP / ScrollTrigger | 3.15 | 滚动进入、按钮磁吸、逐字动画 | 布局时机、清理、降级 |
| Motion | 13.2 | BlurText 动画 | 声明式动画与 reduced motion |
| Canvas 2D | 浏览器原生 | 海洋加载器的粒子球静态绘制 | DPR、重绘成本、合成动画 |
| React PageFlip | 2.0.3 | AI 发展史翻页书 | 受控页码、移动端交互 |
| Pretext | 0.0.8 | 标题按真实布局分行 | ResizeObserver、文本布局 |
| Playwright | 1.63 | 三语、路由、移动端和异常场景回归 | 可观察行为、测试矩阵、失败证据 |
| Oxlint | 1.79 | 静态检查 | lint 与测试的职责区别 |
| Cloudflare Pages | 静态托管目标 | CDN、SPA 回退、静态资产发布 | 直接刷新、缓存和产物限制 |
# 已安装不等于核心使用
面试时不要把 package.json 中的每个依赖都说成“核心技术”。例如 liquid-glass-react 在依赖表中,但当前主要玻璃卡片是项目自己的 SVG filter 和 CSS 实现;它不应该被描述成整站架构核心。src/ocean 中还有一套可由 Vite 处理的海洋源码,但首页的实际调用链是 /ocean-demo/index.html 的隔离预构建运行时。准确区分“依赖存在”“源码存在”和“线上调用链正在使用”会显得更可信。
# 四 React 19 与组件设计
# React 在项目里解决什么
React 负责把五个页面、共享页头页脚、语言状态、弹窗、加载状态和复杂场景外壳组合起来。3D 引擎不是 React 的替代品:React 管理 DOM 和生命周期,Babylon/Cesium 管理 Canvas 内部的渲染世界,两者通过 ref、状态和 Effect 边界协作。
# 为什么大量使用 useRef
3D Viewer、Scene、动画帧 ID、Observer、计时器和指针控制器都不适合直接放进 React state。它们变化频繁,但变化本身不应触发组件重新渲染;useRef 可以保存跨渲染的可变引用,并在 Effect 清理中精确释放。
# Effect 的正确边界
- 创建浏览器外部资源,例如事件监听、Observer、GSAP 时间线、Babylon Engine、Cesium Viewer。
- 组件卸载或依赖变化时撤销同一批资源。
- 使用
disposed或cancelled标记,避免异步加载完成后更新已卸载组件。 - 取消
requestAnimationFrame、setTimeout、setInterval,断开ResizeObserver与IntersectionObserver。 - 销毁粒子系统、纹理、Scene、Engine 和 Viewer,避免 GPU 资源泄漏。
# StrictMode 会带来什么
开发环境中 StrictMode 会额外执行渲染和 Effect 的设置清理流程,帮助暴露副作用不对称的问题。3D 场景如果没有完整 cleanup,可能出现两个渲染循环、重复 Canvas、重复事件或显存不释放。这个项目的复杂组件都显式维护销毁函数,正是为了让生命周期可重入。
1. 为什么不用 React state 保存 Cesium Viewer?参考答案
Viewer 是可变的第三方运行时对象,不参与 JSX 计算,也不需要每次变化都触发 React 渲染。我把它保存在 Effect 闭包或 ref 中,由 React state 只保存 loading、ready、error 和 selected 这类真正影响界面的最小状态。这样能避免无意义重渲染,也明确了销毁责任。
2. `useEffect` 和 `useLayoutEffect` 在项目中怎样选?参考答案
普通事件订阅、异步加载和 3D 初始化放在 `useEffect`,避免阻塞浏览器绘制。需要在浏览器显示前读取布局或安装会影响初始动画状态的逻辑,例如 GSAP 进入动画和轨道尺寸同步,使用 `useLayoutEffect`,减少首帧闪动。两者都必须返回对称清理函数。
3. React 负责 3D 场景里的每一帧吗?参考答案
不负责。React 只负责创建容器、状态提示和交互控件,Babylon 或 Cesium 自己维护渲染循环。每帧位置更新直接发生在引擎对象上;如果每帧都 setState,会让 React 协调和 DOM 更新进入高频路径。只有用户可见的低频状态才回到 React。
4. 为什么国际化状态使用 `useSyncExternalStore`?参考答案
语言状态由模块级 Store、URL、localStorage 和 storage 事件共同驱动,不属于单个组件。`useSyncExternalStore` 提供标准的 subscribe 和 snapshot 接口,让多个页面读取一致快照,也为服务端快照预留了接口。这里也可以用 Context,但外部 Store 更直接地表达“浏览器外部状态源”。
# 五 手写 SPA 路由与 History API
# 为什么没有使用 React Router
项目只有五条固定一级路径,没有动态参数、嵌套路由、路由级数据加载和复杂权限守卫。手写路由只承担路径映射、站内链接拦截、前进后退、旧 Hash 迁移和语言参数保留,代码量可控。它是当前需求下的简单方案,不代表大型项目也应该手写路由。
# 直接刷新为什么可能 404
客户端跳转时,浏览器已经拿到 index.html,React 可以按 pathname 选择页面。直接访问 /about 时,请求先到静态服务器;如果服务器寻找真实的 /about 文件就会 404。所以 Nginx 需要 try_files ... /index.html,Cloudflare Pages 通过 _redirects 的 /* /index.html 200 做 SPA fallback。
# 当前路由方案的边界
- 只认
PAGE_PATHS白名单,不支持参数化路由。 - 页面组件是静态 import,没有路由级代码分割。
- 没有路由守卫、loader、action、错误边界和滚动位置恢复策略。
- document click 委托要小心修饰键、下载链接、Hash 链接、跨域链接和 target。
- 页面数量增长后应迁移到成熟路由库。
5. `pushState` 会自动触发 `popstate` 吗?参考答案
不会。`pushState` 和 `replaceState` 本身不触发 `popstate`,浏览器前进后退才会触发。项目的 `navigate` 在 pushState 后主动派发一个 PopStateEvent,复用同一条路径更新逻辑。更大型的路由器通常有自己的 history 订阅层。
6. 为什么旧 Hash 路由用 `replaceState` 迁移?参考答案
旧书签 `/#/about` 仍然要可用,但用户进入后应该看到干净的 `/about`。用 replaceState 不会额外增加一条历史记录,用户按返回键不会在旧地址和新地址之间多退一步。
7. 什么时候应该换成 React Router?参考答案
出现嵌套路由、动态参数、权限守卫、路由级数据加载、错误页、代码分割或几十个页面时,成熟路由器的约定和测试价值会超过这几十行手写代码。当前方案的价值是需求简单,而不是重复造轮子本身。
# 六 三语国际化与状态一致性
# 国际化设计
中文原文直接作为 Key,英文与日文放在 messages.json。这种方式让中文页面开发直观,但也有缺点:修改中文措辞就等于修改 Key,可能导致其他语言查找失败。规模继续增长时,更稳妥的方式是使用稳定语义 Key,例如 home.hero.title,并让三种语言都进入资源文件。
# URL 为什么优先于 localStorage
URL 表达当前页面可分享的状态。别人打开 ?lang=en 应该得到英文,即使自己浏览器以前保存的是中文。localStorage 只代表设备偏好,不能覆盖明确链接意图。
# 为什么所有存储读写都包在 try/catch
隐私模式、浏览器策略或测试注入可能让 localStorage 访问抛异常。如果语言系统把存储当成硬依赖,整个页面会在启动阶段失败。项目把它当成可选增强:不可用时 URL 仍工作,最后回退中文。
8. 为什么不直接用 `navigator.language`?参考答案
浏览器语言只能作为推测,不能覆盖用户明确选择,也不保证内容团队支持完整地区变体。当前优先级是 URL、已保存选择、中文默认值,行为确定且便于测试。后续可以在首次访问且没有任何偏好时把 navigator.language 作为软提示。
9. 模块级 Store 有什么风险?参考答案
它在单页客户端中简单有效,但如果做 SSR,模块实例可能跨请求共享,必须把请求级状态隔离;如果热更新或多 Bundle 重复加载,也要注意是否出现多份 Store。当前是纯客户端静态站,所以这个边界可以接受。
10. 怎样发现英文页面遗漏了中文?参考答案
Playwright 在英文页面遍历 main 下的文本节点,排除语言切换器和脚本样式后,用 Unicode Han Script 正则寻找汉字,期望结果为空。它不能证明翻译质量,但能自动发现大量最常见的漏翻。
# 七 Vite、TypeScript 与资源管线
# Vite 在项目中的职责
- React JSX 转换与开发时快速更新。
- 将图片、字体、CSS 和 JavaScript 产出到
dist。 - 通过自定义插件把
.wgsl文件作为源码字符串导入。 - 把
.exr、.hdr、.glb识别为静态资源 URL。 - 使用
vite-plugin-static-copy复制 Cesium 运行时目录。 - 保留
public目录中的海洋预构建页面、WASM、模型、海报和 PDF。
# WGSL 为什么不能按普通资源 URL 处理
Babylon ComputeShader 需要的是着色器源代码字符串。普通资源导入通常返回构建后的 URL,因此项目插件在 transform 阶段读取 WGSL 内容,生成 export default "...shader source..."。这属于一个很小但有明确抽象价值的构建适配层。
# TypeScript 的真实使用边界
项目配置 allowJs: true、strict: false,React 页面主要是 JSX,海洋计算模块主要是 TypeScript。因此可以说“项目混合使用 JS/TS,并在图形计算模块获得类型帮助”,不能说“全项目完成严格 TypeScript 类型治理”。如果面试官问改进,应回答开启 strict、逐步补全组件 Props、事件类型和第三方运行时接口,而不是一次性强改全部文件。
11. 为什么 Cesium 资源要复制 Workers、Assets 和 Widgets?参考答案
Cesium 不只有一个 JavaScript 文件,运行时还会按相对路径加载 Web Worker、纹理与数据资源、控件样式和第三方模块。项目设置 `CESIUM_BASE_URL`,再把对应目录复制到相同发布结构,否则主脚本可能加载成功,但地球、地形处理或控件在运行时 404。
12. `public` 和源码 import 的资源有什么区别?参考答案
源码 import 的资源会进入构建依赖图,通常获得 Hash 文件名和引用改写;public 资源按原路径复制,适合必须保持固定 URL 的预构建海洋页面、模型目录和下载文件。代价是缺少按 import 图自动发现与强缓存文件名,需要额外做存在性和缓存管理。
13. 当前构建最大的工程问题是什么?参考答案
现有 dist 显示主 JavaScript 大约 7 MiB,整个产物约 98 MiB,最大的单个 GLB 约 23.68 MiB,已经接近 Pages 的 25 MiB 单文件限制。下一步应做页面级动态 import、把 Babylon 粒子和 Cesium 进一步拆分、压缩或替换旧海洋大模型,并用 Bundle Analyzer 和真实网络瀑布图设性能预算。
# 八 Babylon.js、WebGL 与 WebGPU
# Babylon.js 是什么
Babylon.js 是浏览器 3D 引擎,封装场景树、相机、灯光、材质、纹理、模型、粒子、动画以及 WebGL/WebGPU 后端。项目没有直接用底层 API 从零管理所有 GPU 状态,而是在引擎抽象上实现两个不同任务:主站的图标粒子场,以及隔离运行的 FFT 海洋。
# WebGL 与 WebGPU 的区别
WebGL 基于 OpenGL ES 图形管线,浏览器支持范围更广,适合普通顶点、片元渲染。WebGPU 更接近现代 GPU API,除了渲染管线,还原生提供 Compute Shader、Storage Buffer/Texture 和更明确的资源绑定模型。FFT 海洋需要大量规则的并行计算,因此 WebGPU 计算着色器更合适;课程粒子场使用 Babylon 的普通 WebGL 能力即可。
# 为什么两个 Babylon 版本要隔离
上游 OceanDemo 使用 Babylon 5.x,主站依赖 Babylon 9.23。强行把旧 Demo 源码直接升级或让两个版本共享页面,会把引擎 API 差异、全局 BABYLON、Shader 注册、材质扩展和资源缓存耦合在一起。iframe 提供独立 Window、模块运行时、Canvas 和全局对象,父页面只通过明确的 contentWindow.scene 集成点配置相机、天空和船模。
这不是没有代价:iframe 增加了一层通信与可测试性成本,父页面访问子页面还依赖同源,构建和升级要维护两套产物。当前选择的原因是先降低版本冲突和迁移风险;长期方案是把 OceanDemo 升级到统一 Babylon 版本并形成稳定模块接口。
# 课程粒子场怎样工作
AdmissionsParticles 在 Canvas 上创建 Babylon Engine、Scene、ArcRotateCamera 和多层粒子系统。技术图标先绘制到动态纹理或加载图标纹理,再分布在三维空间。每帧根据时间、速度和范围更新位置,并把粒子尺寸换算成与相机深度相关的世界单位,避免靠近相机时图标无限放大。移动端调整相机半径与粒子密度,卸载时销毁粒子、纹理、场景和引擎。
14. Scene、Engine、Camera 和 Mesh 分别是什么?参考答案
Engine 连接浏览器 Canvas 和 GPU,管理渲染后端;Scene 是一组相机、灯光、网格、材质和更新逻辑的容器;Camera 决定视图和投影;Mesh 提供几何顶点及其世界变换。项目卸载时先销毁场景内资源,再销毁 Engine,避免渲染循环和 GPU 对象残留。
15. 为什么不能只检测 `navigator.gpu` 就认为海洋一定可用?参考答案
`navigator.gpu` 只说明浏览器暴露了入口,不代表适配器、设备、所需纹理格式和计算着色器一定成功。项目还会检查 Babylon Engine 的 `supportComputeShaders`,监听 iframe 与场景失败,并以真实海面 draw call 作为就绪条件。能力检测必须分层,不能只看一个布尔值。
16. iframe 隔离与微前端有什么相似和不同?参考答案
相似点是独立运行时和依赖隔离;不同点是这里没有团队级应用拆分、独立路由和部署治理,只是为了隔离一个旧 3D 运行时。父子页面同源,父页面仍直接读取子页面场景并做有限配置,所以它更像技术兼容层,不应包装成完整微前端架构。
17. 为什么 3D 场景必须显式 dispose?参考答案
JavaScript 对象被回收不等于 GPU 纹理、Buffer、Shader、Canvas 事件和动画帧立即释放。单页应用反复进出页面时,如果只移除 DOM,会持续占用显存和 CPU。项目在 cleanup 中取消渲染循环、断开监听,并 dispose 粒子、纹理、Scene、Engine 或 Cesium Viewer。
# 九 FFT 海洋与计算着色器
# 先用一句话理解
FFT 海洋不是逐个顶点随便加正弦波,而是在频域组合大量不同方向和尺度的波,再用二维逆 FFT 高效还原成每一帧的空间位移、斜率和泡沫数据。
# 核心流程
- 生成高斯噪声,为不同频率的波提供随机初始相位。
- 根据风速、风向、重力和波谱模型计算初始频域振幅。
- 按时间更新复数频谱,使波随色散关系演化。
- 对多个位移和导数纹理执行二维逆 FFT。
- 合并得到水平位移、垂直位移、法线相关数据和破碎趋势。
- 海面几何在顶点着色阶段读取位移纹理,材质读取法线、反射、折射与泡沫信息。
- 浮力模块在对象位置采样波面高度和方向,让浮标或船随浪运动。
# 为什么使用 FFT
假设海面网格为 N×N,如果对每个空间点遍历所有频率,直接离散傅里叶变换的成本非常高;FFT 把一维复杂度从 O(N²) 降到 O(N log N),二维通过先横向再纵向执行。GPU 可以把每一阶段的大量元素并行处理,适合实时视觉。
# Compute Shader 如何执行
WGSL 定义工作组大小和每个 invocation 处理的纹理坐标。JavaScript 创建 ComputeShader,绑定输入纹理、Storage Texture、Uniform Buffer 和预计算数据,再根据数据尺寸除以线程组尺寸计算 dispatch group 数。IFFT 横向和纵向使用 ping-pong 纹理交替读写,避免同一阶段同时读写同一资源产生冲突。
# 海面几何为什么分块
无限海面不可能真的创建无限网格。常见做法是以相机为中心使用多个不同分辨率和尺度的网格块,近处细、远处粗,并随相机调整位置,形成视觉上的无限延伸。项目的 OceanGeometry 管理分块、顶点数据、边缘过渡和相机关系。
# 船为什么能随浪动
项目没有再做一套昂贵的船体流体模拟,而是复用上游浮标的波面采样结果。原浮标根节点已经获得位置与旋转,船模被挂到新 carrier,逐帧复制浮标位置,再加入缓慢前进距离;旋转用 Quaternion Slerp 在船的基础朝向和波浪朝向之间插值,只保留约 62% 的波浪姿态,兼顾真实感与甲板可见性。
18. FFT 和 IFFT 的区别是什么?参考答案
FFT 是高效计算离散傅里叶变换的一类算法,不只代表正向变换;IFFT 是从频域回到空间域的逆变换。海洋先在频域构造和演化波谱,再通过二维 IFFT 得到每个空间位置的位移与导数。
19. 为什么需要 ping-pong 纹理?参考答案
FFT 有多个阶段,当前阶段的输出是下一阶段的输入。交替使用两张纹理可以避免同一次 dispatch 在没有同步保证的情况下边读边覆盖同一资源,也减少每阶段重新分配纹理的成本。
20. 海洋算法是你原创的吗?参考答案
不是从零原创。底层 FFT Ocean 来自开源 OceanDemo,它又参考了 Unity FFT Ocean。我负责的是把它以可维护方式接入站点:隔离引擎版本、本地化远程运行时、重做首帧判定和降级、调整相机与天空、压缩船模、复用浮力、同步海天线,并验证响应式与失败路径。我会明确区分上游算法和自己的工程贡献。
21. 为什么 iframe 的 load 事件不能代表海洋 ready?参考答案
load 只代表文档及其同步依赖完成加载,WebGPU 设备初始化、WGSL 编译、纹理上传、模型加载和第一帧真正绘制可能还在后面。项目等待场景存在、计算能力可用,并观察海洋网格实际发生有效 draw call,再经过两帧合成才通知 Loader 结束。
22. 30 秒超时为什么不直接判失败?参考答案
低性能设备首次编译 WebGPU Shader 可能很慢,慢不等于失败。30 秒后先显示静态预览,保证用户可看到内容,但让 iframe 在后台继续准备;真正完成后自动切到动态海洋。只有能力明确缺失或初始化错误才进入失败状态。
23. 如何避免船模穿浪?参考答案
先复用同一波面采样,保证船和海浪处于同一事实源;再调整船体局部水线高度,使用四元数插值降低极端 pitch 和 roll,并在多个波相位观察甲板与浪峰关系。它是视觉浮力,不是完整刚体流体仿真,所以应描述为稳定的近似。
# 十 Cesium 三维地球与测地线
# Cesium 为什么适合这个模块
普通 Three.js/Babylon 当然也能画球,但 Cesium 原生提供 WGS84 椭球、经纬度到笛卡尔坐标转换、测地线插值、相机与地球表面拾取,以及为地理可视化设计的 Primitive。项目需要从北京到多个城市的全球航线,因此使用 Cesium 能减少地理数学和交互基础设施的重复实现。
# 航线怎样生成
每条路线用 EllipsoidGeodesic 连接北京和目的地,按 193 个采样点插值经纬度,再根据路线距离计算一个受上下限约束的最大高度,用 sin(πt) 让中点最高、两端回到地表。随后创建光晕线、主体线、移动尾迹、头部点和目的地标签。
# 为什么开启 requestRenderMode
Cesium 默认适合连续动态世界,会持续渲染。这个模块大部分内容静止,只在航线运动、自转、相机飞行、尺寸变化或资源加载时需要新帧。requestRenderMode 加自定义循环可以把控制权交给页面,在不可见或文档隐藏时停止推进,降低 CPU/GPU 消耗。
# ready 为什么还要检查地球表面
Viewer 创建成功不代表用户看到的地球已经贴图。项目等待至少几个 postRender、tilesLoaded,并从屏幕中心发射拾取射线确认确实命中 Globe,才把状态设为 ready。这避免静态预览过早消失,露出黑球或未完成纹理。
24. 大圆航线和屏幕直线有什么区别?参考答案
地球表面两点最短路径应沿球面或椭球测地线,不是把经纬度在线性平面上直接连线。Cesium 的 EllipsoidGeodesic 在 WGS84 椭球上插值,再把每个点转为三维坐标。项目额外加入高度形成可视弧线,但地面路径仍遵循测地线。
25. 为什么要用 `IntersectionObserver` 懒加载地球?参考答案
Cesium 脚本、Worker、纹理和 Viewer 初始化成本高,而地球位于页面下方。Observer 在接近视口时才启动,能减少首屏网络、解析和 GPU 竞争。它比在 scroll 回调中频繁读布局更适合可见性判断。
26. 自定义 45 FPS 有什么取舍?参考答案
航线头部和缓慢自转不要求 60 FPS,45 FPS 可以降低持续负载,又比 30 FPS 更平滑。代码按时间差推进而不是按帧数推进,并把 delta 上限限制为 0.1 秒,避免切回页面后一次跳很远。最终帧率仍应根据设备测量动态调整。
# 十一 动画、Canvas 与视觉组件
# GSAP 与 ScrollTrigger
SiteMotion 对现有页面元素做滚入动画,不额外包裹固定页头或 3D Canvas。它根据选择器批量创建 tween,卡片使用位移、透明度、轻微旋转和缩放;动画只执行一次。gsap.matchMedia 只在用户没有减少动态偏好时启用,cleanup 时 revert 所有媒体上下文。
# 为什么焦点进入时强制完成动画
键盘用户可能通过 Tab 聚焦到仍然 opacity 0 的按钮。如果只考虑滚动视觉,元素存在于可访问树但肉眼不可见。项目监听 focusin,一旦焦点进入动画元素,就把对应 tween 推到完成状态,保证键盘焦点可见。
# Canvas Loader 的性能演进
加载器最初可以持续重绘粒子球,但此时海洋正在编译 Shader,两者会争抢主线程与 GPU。当前做法是 Canvas 只绘制一次粒子球,持续呼吸交给 CSS compositor 的 opacity/scale,进度每 100ms 左右低频更新。视觉上保持活动感,但不在最昂贵阶段制造另一条高频绘制链。
# 其他视觉库
- Motion 的
BlurText负责声明式逐字模糊进入,并读取 reduced motion。 - React PageFlip 提供翻页书;React state 记录当前页并同步左右控制。
- Pretext 根据真实宽度重新分行,
ResizeObserver在容器变化时更新。 - SpotlightCard 用指针位置写 CSS 变量,CSS 完成聚光与倾斜;触摸和 reduced motion 不启用。
- Simple Icons 与 Phosphor Icons 提供真实品牌和界面图标,避免用 emoji 代替产品图标。
27. 为什么优先动画 transform 和 opacity?参考答案
它们通常可以在合成阶段完成,避免频繁触发布局和大范围绘制。并不是所有 transform 都零成本,大面积图层仍会占显存,所以项目还要控制动画数量、模糊半径和同时运行的 3D 场景。
28. `requestAnimationFrame` 节流鼠标光场有什么意义?参考答案
pointermove 频率可能高于屏幕刷新率,直接每次修改样式会产生重复工作。把最新坐标合并到下一次 animation frame,保证一帧最多更新一次,并与浏览器绘制节奏对齐。
29. reduced motion 应该怎样处理?参考答案
不是简单把所有内容隐藏,而是保留信息和最终状态,关闭或缩短滚动、逐字、自动旋转和高频运动。项目通过 CSS 媒体查询、GSAP matchMedia、Motion 的 useReducedMotion 和 3D 组件里的 matchMedia 分层处理。
# 十二 性能优化与渐进降级
# 性能预算要分四类看
| 类型 | 主要风险 | 项目措施 |
|---|---|---|
| 网络 | 大模型、纹理、Cesium 与主 JS | 本地压缩船模、WebP、接近视口加载、静态预览 |
| JavaScript | 解析执行、主线程事件、Bundle 大 | 低频状态、第三方运行时延迟加载、Observer |
| GPU | 多 Canvas、Shader 编译、像素填充 | 帧率上限、DPR 限制、不可见暂停、降级图 |
| 内存 | 纹理、Buffer、Viewer、事件泄漏 | Effect cleanup、dispose、取消动画帧与 Observer |
# 当前已经做的优化
- 船模型从约 24.8 MiB 压缩到约 3.2 MiB,贴图由 2048×2048 重采样到 512×512。
- Cesium 进入视口附近才初始化,并将连续默认循环改成按需渲染。
- 地球动画限为约 45 FPS,页面隐藏或模块不可见时停止推进。
- 海洋慢加载时先显示高质量静态预览,避免白屏。
- Loader Canvas 使用 DPR 1 并取消持续全屏重绘。
ResizeObserver只在尺寸变化时处理,事件卸载时移除。- 移动端减少粒子数、调整相机和布局,短横屏也有专项断点。
- 所有重动画考虑
prefers-reduced-motion。
# 当前仍然存在的问题
- 主 JavaScript Bundle 约 7 MiB,说明页面没有真正路由级拆包,Babylon 等依赖可能进入主包。
- 产物约 98 MiB,旧 OceanDemo 带有多个大 GLB、WASM、EXR 和 HDR。
- 最大单文件 23.68 MiB,只比 Cloudflare Pages 25 MiB 限制低约 1.32 MiB,安全余量小。
- 没有基于网络条件、设备内存、GPU 型号做更细的质量分级。
- 没有真实 RUM、Core Web Vitals 和错误采集,只能依赖本地 QA 与浏览器测试。
- 首页海洋 iframe 的跨运行时协调仍有轮询,可改成显式
postMessage协议。
# 如果让你继续优化
第一步不是盲目重写,而是采集 Bundle 构成、LCP、INP、CLS、内存和 GPU 帧时间。第二步做页面级 React.lazy 和动态 import,把 Babylon 粒子、Cesium 和 PageFlip 从首屏主包分开。第三步清理未使用依赖和重复海洋资产,把大模型转换为 meshopt/Draco、KTX2/Basis 纹理,并设置长期缓存。第四步按设备能力选择实时海洋、简化 WebGL 海面或静态图。最后接入 RUM,确认优化对真实设备有效。
30. 懒加载是不是越晚越好?参考答案
不是。太晚会让用户滚到模块时看到明显等待。项目给 IntersectionObserver 设置正 rootMargin,在进入视口前预热。具体距离要结合资源体积、网络速度和用户滚动速度测量。
31. 静态预览是否掩盖了真实性能问题?参考答案
它改善感知体验,但不能替代性能优化。必须分别测量预览出现时间、实时场景可交互时间、下载体积和失败率。项目把慢与失败分开,并保留状态提示;后续仍应压缩资源和做能力分级。
32. 如何定位 3D 页面卡顿?参考答案
先区分网络、主线程和 GPU:看瀑布图与资源解码,Performance 面板的脚本和布局,GPU 帧时间、draw call、三角形、纹理与 DPR。然后逐项关闭地球、粒子、阴影、后处理和高分辨率,找最敏感因素。只看平均 FPS 不够,还要看长任务和低端设备。
33. 为什么不能只依靠 tree shaking 解决主包过大?参考答案
Tree shaking 只能移除可静态判断的未使用导出,实际使用的 Babylon、动画和页面代码仍会进同一 Chunk,第三方包也可能有副作用。真正减少首屏下载还需要动态 import、路由拆包、替代重依赖和资源压缩。
# 十三 响应式、无障碍与内容合规
# 响应式策略
全站共享的最终断点集中在 Responsive.css,主要覆盖桌面、平板、760px 手机、320px 小屏和短横屏。不是只改变列数,还调整导航形态、3D 相机距离、粒子密度、标题分行、触摸目标、安全区和浮动元素位置。
# 无障碍措施
- 页面使用真实 heading、link、button,不用 div 模拟所有交互。
- 图标动画元素通常
aria-hidden,Canvas 和交互模块提供可读标签。 - 弹窗使用 dialog 语义,并提供关闭按钮和明确图片 alt。
- 菜单支持 Escape 关闭;按钮保留 focus 样式。
- 动画尊重 reduced motion;键盘焦点不会停在透明元素上。
- 地球目的地同时提供按钮列表,不要求用户只能拖拽 Canvas。
- 状态变化使用
role=status或aria-live。
# 当前无障碍不足
海报弹窗还应系统验证焦点陷阱、打开后的初始焦点、关闭后的焦点恢复和背景 inert;3D Canvas 的完整等价文本仍可以更丰富;颜色对比需要用自动工具和人工复核;真机读屏和键盘全流程尚未形成正式验收记录。
# 内容与素材合规
项目保留字体许可证、React Bits 许可证、OceanDemo 许可证、Babylon Runtime 许可证、船模来源和地球纹理来源。宣传文案做过广告法风险复查,但图片海报中的部分表述和讲师履历仍需要真实材料支持。技术面试中可以说“建立了素材来源和文案风险清单”,不能说“已经获得完整法律合规认证”。
34. 响应式为什么不能只用一个 768px 断点?参考答案
页面不仅有普通卡片,还有固定导航、长中英日标题、3D Canvas、翻页书和短横屏。设备宽度相同但高度、输入方式和语言长度不同。项目用内容实际破坏点决定断点,并对 320px 和短横屏做专项检查。
35. Canvas 内容怎样做无障碍?参考答案
先判断它是装饰还是功能。装饰 Canvas 应从可访问树隐藏;功能 Canvas 需要可读名称,并在 DOM 中提供等价操作和信息,例如地球的目的地按钮与文字读数。不能指望读屏理解像素。
# 十四 测试、质量与部署
# Playwright 测试矩阵
当前测试代码定义 34 个用例:五条路径×三种语言×两种宽度共 30 个,再加四个交互与异常用例。核心检查包括:
html lang与页面标题正确。- 首页链接、页脚内容和页面结构存在。
- 英文 main 区域没有遗漏汉字。
- 字体 ready 后横向溢出不超过 2px。
- 监听未捕获 page error。
- 语言切换后导航、刷新、历史返回保持一致。
- 移动菜单与海报语言同步。
- 折叠图片支持 click 和键盘 Enter,不因 hover 意外改变。
- 非法语言回退;localStorage 抛错时页面仍可工作。
# 为什么浏览器测试不等于全部质量
测试使用 Chrome channel 和 reduced motion,不能代表 Safari、Firefox、Android GPU、iOS WebKit 与真实触摸设备。海洋 WebGPU 尤其依赖浏览器和驱动。自动化还没有覆盖每种 3D 失败、内存泄漏、视觉像素差异和网络弱网,因此项目保留了人工 QA 记录和明确边界。
# 产物检查
check-pages.mjs 递归统计 dist:文件数不能超过 20000,单文件不能超过 25 MiB,并检查 index.html、_redirects、关键海洋图、Cesium 主脚本和三语海报存在。当前已有 dist 检查结果是 845 个文件,最大文件约 23.68 MiB。
# 当前环境验证状态
本次整理文档时,单独运行已有 dist 的产物检查成功。生产构建、Oxlint 和需要启动 Vite preview 的 Playwright 未能重新执行,因为现有 node_modules 缺少 Rolldown 与 Oxlint 的 macOS 可选原生绑定。这个问题指向依赖安装状态,不代表源码测试一定失败;但在重新 npm ci 并实际跑通前,不能声称本次全量验证通过。
# Cloudflare Pages 部署
项目是纯静态站,不需要后端和环境密钥。部署产物是 dist,_redirects 处理 History 路由直接刷新。发布后应逐条检查五个路径的直接打开和刷新、三语切换、海报下载、二维码和 3D 场景。自定义域名尚受域名账号问题影响,不能把“具备部署配置”说成“正式域名已上线”。
36. 单元测试、集成测试和 E2E 在这个项目怎么分?参考答案
纯函数如翻译替换、海天线平滑和路径规范化适合单元测试;React 与 3D 外壳的状态切换适合集成测试;语言、导航、弹窗和刷新属于 Playwright E2E。当前 E2E 较完整,但单元和组件测试仍不足,是后续改进点。
37. 为什么测试要等待 `document.fonts.ready` 再检查溢出?参考答案
后备字体和最终字体的字宽不同。如果过早测量,测试可能在后备字体下通过,真实字体加载后却溢出,或反过来产生不稳定结果。等待字体完成能让布局断言更接近用户最终看到的状态。
38. 生产构建失败但旧 dist 检查通过,说明什么?参考答案
只能说明现有 dist 满足文件约束,不能证明当前源码能重新构建。旧产物可能来自之前正常的依赖环境。交付前必须修复依赖安装并重新 build,再对新 dist 执行检查,不能用旧产物替代源码可构建性。
39. 为什么选择静态部署而不是服务器?参考答案
当前业务没有登录、数据库和服务端动态内容,静态部署能减少运维面、密钥和攻击面,并利用 CDN 分发。代价是运行时 SEO、多语言预渲染和动态内容能力有限;需求变化后再引入 SSR 或 API,而不是为假想需求先上服务器。
# 十五 “最难的问题”标准回答
面试官问难点时,不要回答“时间紧”“需求经常改”后就结束。一个有技术含量的回答应包含:现象、错误假设、根因、约束、方案、验证和边界。以下五个版本只选最符合你真实经历的两到三个,改成自己的口语。
# 难点一 WebGPU 海洋真实就绪与渐进降级
一句话结论
项目最难的不是把海洋 Demo 放进页面,而是定义“什么时候它真的可展示”,并让不支持、加载慢和初始化失败三种情况都有合理体验。
两分钟回答
最初很容易把 iframe 的 load 或 Scene 创建成功当成 ready,但实际会出现 Loader 消失后只有天空、黑帧或还没编译好的海面。根因是 WebGPU 设备初始化、WGSL 编译、纹理上传和第一帧有效绘制发生在文档 load 之后,而且不同设备差异很大。
我的处理分四层。第一层先看 navigator.gpu,没有入口直接静态图。第二层等 iframe 场景实例并检查 Babylon 的 compute shader capability,避免把诊断场景当成成功。第三层在相机和材质配置完成后监听渲染,只有海洋网格真的产生有效 draw call 才标记 ready。第四层再等两个 animation frame,让浏览器完成 iframe 显示与合成后才结束 Loader。
同时我把慢和失败分开:30 秒只是显示预览,后台继续准备;明确失败才停止实时场景并给重试按钮。这样兼顾了首屏可见性和高端设备效果。边界是现在父子通信仍有轮询,后续会改成 iframe 内部通过 postMessage 发布 booting/surface-ready/error 状态,并记录各阶段耗时。
可能追问
- 为什么不是固定 5 秒后显示?因为设备差异大,时间不等于状态。
- 为什么要双 RAF?第一帧提交 DOM 状态,下一帧更有机会看到已合成结果。
- 为什么不直接取消慢加载?慢设备可能只是首次 Shader 编译,取消会让用户永远无法看到实时场景。
# 难点二 Babylon 5.x 与 9.x 的运行时隔离
一句话结论
我没有把旧海洋代码硬升级后直接塞进主包,而是先用 iframe 把版本风险限制在一个边界内,再通过小接口完成站点化改造。
两分钟回答
海洋上游基于 Babylon 5.x,主站粒子场已经使用 Babylon 9.x。两个版本在引擎 API、材质扩展、Shader 注册和全局对象上存在潜在冲突。如果一次性升级整个 OceanDemo,既要验证 FFT 计算正确性,又要验证所有画面和资源,改动面太大。
我选择把上游构建保留成同源 iframe。它拥有独立 Window、Canvas 和依赖,主站只负责等待 Scene、固定相机、调整天空、替换船模和接收就绪信号。这个方案把迁移风险和主站迭代解耦,同时保留了回退图。
我也会主动说明代价:两套 Babylon 增加下载体积,父页面直接访问子页面对象不是理想公共接口,同源限制也较强。长期演进是先补海洋视觉回归和性能基线,再逐步把上游升级到 Babylon 9,最终改成动态 import 的独立模块或 Worker/OffscreenCanvas 可控边界。
# 难点三 重 3D 体验与首屏性能之间的平衡
一句话结论
我的策略不是一刀切删除 3D,而是按用户可见性和设备能力安排加载顺序,让信息先可用,增强体验后到。
两分钟回答
首页有 WebGPU 海洋,课程页有 Babylon 粒子,公开课页有 Cesium 地球。如果它们同时进入首屏主链路,会争抢网络、JS 解析、GPU 和显存。我先把内容层与 3D 层分离,所有关键信息都用正常 DOM;海洋始终有静态预览,地球进入视口才加载,粒子和地球在卸载时完整释放。
运行时方面,地球关闭默认连续渲染并限制 45 FPS,页面隐藏或不可见时不推进;Loader Canvas 从持续重画改成一次绘制加 CSS 合成;移动端降低粒子量和相机负荷;所有自动运动响应 reduced motion。资源方面压缩船模,并用构建脚本检查 25 MiB 单文件上限。
验证上,除了看 FPS,我会看首屏静态内容出现、实时场景 ready、主线程长任务、资源瀑布和 GPU 占用。当前最大的遗留问题是主包和总体积仍大,所以不能回答“性能已经彻底解决”,而应说明下一步是代码分割和 3D 资源压缩。
# 难点四 三种语言、路由和资源状态一致性
一句话结论
难点不只是翻译文案,而是保证语言在分享链接、站内导航、刷新、历史返回、弹窗资源和存储不可用时都只有一份一致状态。
两分钟回答
我把 URL 作为明确意图,localStorage 作为偏好,模块 Store 作为运行时快照。首次进入按 URL、存储、中文默认的优先级解析;切换语言用 replaceState 更新当前 URL,不增加多余历史;站内路径统一通过 localizedPath 携带语言。浏览器前进后退和跨标签 storage 变化也会同步 Store。
课程海报不是只换按钮文字,还要切换实际图片、下载文件名和 dialog alt。Playwright 因此不仅断言某个标题,而是覆盖五页、三语、桌面和手机,并额外测试导航、刷新、回退、海报切换和 localStorage 抛异常。当前不足是中文句子作为 Key,不利于长期维护,规模扩大后会迁移到稳定语义 Key 和翻译完整性检查。
# 难点五 3D 海天线与 DOM 视觉层精确对齐
一句话结论
我没有把暖色遮罩的位置完全写死,而是把 3D 世界中的海平面远点投影到屏幕,再用低通式限制更新写给 CSS 变量。
两分钟回答
首页 DOM 上有文字、渐变和氛围层,iframe 里有独立 3D 相机。只用固定百分比对齐,在不同宽高比和相机矩阵下会偏离真实海天线。我的做法是取相机水平前向,在 y=0 海平面上选择很远的点,用相机自己的 view 和 projection matrix 投影到 viewport,得到屏幕 y,再更新父容器的 --hero-horizon-y。
这里还有一个坑:Scene 当前 transform matrix 可能在反射探针或上一帧状态,所以我显式用固定主相机的 view×projection 矩阵,而不是盲信 Scene matrix。为了避免每帧像素噪声让 CSS 层抖动,更新函数还会做范围限制和稳定处理。它体现的是跨 Canvas 和 DOM 坐标系的统一,而不只是调 CSS。
# 十六 面试官可能继续深挖的问题
# JavaScript 与浏览器
40. `requestAnimationFrame`、`setTimeout` 和 `setInterval` 怎样选择?参考答案
与屏幕绘制同步的动画使用 requestAnimationFrame;一次延迟动作使用 setTimeout;固定轮询可用 setInterval,但要考虑漂移、重入和页面隐藏。项目对视觉帧使用 RAF,对 Loader 进度和 iframe 场景发现使用计时器,并在 cleanup 中全部取消。
41. 为什么要把 delta 限制到 0.1 秒?参考答案
标签页后台暂停后,下一帧时间差可能很大。如果直接按完整 delta 推进,航线头和相机会突然跳跃。限制单帧最大步长可以让恢复更稳定;需要精确物理时还应使用固定时间步和累积器。
42. 事件委托的优缺点是什么?参考答案
优点是一个 document listener 可以处理动态生成的站内链接,不必每个组件重复绑定;缺点是边界判断更复杂,必须尊重默认阻止、鼠标键、修饰键、target、download、Hash 和跨域。项目只对白名单内部路径接管,其余行为交还浏览器。
43. `ResizeObserver` 与 window resize 有什么区别?参考答案
window resize 只表示视口变化,元素可能因字体、侧栏、内容或父布局改变而变尺寸。ResizeObserver 观察具体元素,适合 Canvas、标题分行和 ScrollTrigger 刷新。回调里仍要避免同步反复改尺寸造成观察循环。
44. `image.decode()` 比只等 load 好在哪里?参考答案
load 表示资源到达,decode 返回的 Promise 在图片可解码时完成,更适合在切换可见层前避免解码卡顿或空白。项目对海洋静态预览先 decode,再等绘制帧后通知 ready。
# CSS 与渲染
45. 浏览器一帧大致经过哪些阶段?参考答案
JavaScript 更新后,浏览器可能进行样式计算、布局、绘制和合成。并非每次都经过全部阶段;只改 transform/opacity 常可跳过布局和大部分绘制。Canvas/WebGL 还有自己的绘制命令和 GPU 管线,最终再与 DOM 图层合成。
46. DPR 为什么会显著影响 3D 性能?参考答案
Canvas 内部像素数约等于 CSS 宽×高×DPR²。DPR 从 1 升到 2,不是两倍而是约四倍像素填充和部分后处理成本。项目限制地球 resolutionScale 和 Loader DPR,保证高分屏清晰度与性能平衡。
47. backdrop-filter 和大模糊为什么贵?参考答案
它们需要对较大区域采样和离屏处理,动态背景下还可能频繁重算,叠加多个大半径模糊会增加 GPU 和显存压力。项目把玻璃和光晕控制在局部,并在重 3D 页面避免无限叠层;是否昂贵最终要通过 DevTools 测量。
# React 与状态
48. 为什么不能在 render 中创建 Babylon Engine?参考答案
render 必须保持纯,可能重复执行或被中断。创建 Engine 会产生 Canvas、GPU 和事件副作用,重复 render 就可能重复创建。应该在 Effect 中创建,并返回完整 cleanup。
49. stale closure 在动画代码里会怎样出现?参考答案
长期运行的 RAF 回调捕获初次渲染的 state,后来按钮更新 state,但回调仍读旧值。项目用 `rotatingRef` 同步最新是否旋转,让每帧读取 ref;React state 只负责按钮显示。也可以重建 Effect,但会增加场景重启成本。
50. 为什么状态要最小化?参考答案
可由其他状态或引擎对象推导的值不应重复保存,否则会出现两份事实不一致。地球只把 loading/ready/error、selected 和 rotating 放入 React,路线点、Primitive 和 Viewer 留在引擎内部。
# 图形学基础
51. View Matrix 和 Projection Matrix 分别做什么?参考答案
View Matrix 把世界坐标变换到以相机为基准的观察空间;Projection Matrix 把观察空间映射到裁剪空间,透视投影会产生近大远小。海天线同步就是用固定相机的 view×projection 把世界海平面点投到屏幕。
52. 四元数为什么适合旋转插值?参考答案
欧拉角直观但可能万向节锁,直接分量插值也会出现不自然路径。单位四元数能稳定表示三维旋转,Slerp 沿球面做平滑插值。项目用它在船的基础朝向和波浪姿态之间取折中。
53. PBR 材质是什么?参考答案
PBR 是基于物理的渲染模型,用粗糙度、金属度、法线、环境光和能量关系得到更一致的材质表现。海面还需要反射、折射、菲涅耳和波面法线。它追求可解释的一致近似,不等于真实流体光学全仿真。
54. Draw Call 为什么重要?参考答案
每次 draw call 都有 CPU 到 GPU 的提交与状态管理成本,很多小对象会让 CPU 成为瓶颈。粒子系统、PolylineCollection 和 PointPrimitiveCollection 能批量管理相似对象。项目还利用 draw call 是否大于零判断海洋表面真正参与了渲染。
# 工程化与部署
55. 为什么锁 Node 版本?参考答案
构建工具和可选原生依赖对 Node ABI、平台和包管理器行为敏感。项目提供 `.node-version` 22.16.0,能减少本地与 CI 差异,但还需要在 CI 真正使用它并通过 `npm ci` 按锁文件安装。
56. `npm install` 和 `npm ci` 有什么区别?参考答案
`npm ci` 按 lockfile 做干净、可重复安装,适合 CI;`npm install` 可能更新锁文件并增量调整 node_modules。当前缺可选原生绑定时,可靠修复通常是在确认没有本地依赖修改后重新做干净安装,而不是手工把某个二进制塞进去。
57. SPA 的 SEO 有什么问题?参考答案
当前 title 和 description 在客户端切换,爬虫是否执行 JS 和如何处理不同 lang URL 不完全可控,也没有每种语言的静态 HTML、hreflang 和社交卡片。营销站如果重视自然搜索,应做 SSG/SSR、独立语言 URL、canonical、结构化数据和 sitemap。
58. Cloudflare Pages 缓存怎样设计?参考答案
带内容 Hash 的构建资产可以长缓存并 immutable;`index.html` 应较短缓存或可重新验证,确保能指向最新 Hash;public 中固定文件名的海洋资产要么版本化 URL,要么谨慎设置缓存并提供更新策略。发布采用原子版本,避免 HTML 与资产不一致。
# 十七 压力追问与防守回答
59. 你这个项目叫 AI 全栈,但没有调用大模型,是否名不副实?参考答案
如果单看这个仓库,它是 AI 课程品牌的体验层项目,不是大模型应用,我不会把课程内容等同于 AI 技术实现。它主要证明复杂前端、图形、国际化、测试和部署能力。我的 AI 能力应由另一个包含模型、RAG 或 Agent 调用链的项目证明;在这份经历里我会准确标成“AI 教育官网”或“AI 全栈课程品牌站”。
60. 为什么不用现成路由库,是不是过度自研?参考答案
当前只有五条静态一级路由,手写层约几十行并覆盖了真实需要的 History、旧 Hash 和语言保留,复杂度可控。但我不把它当通用框架;出现嵌套、参数、数据加载和守卫时会迁移到 React Router。判断标准是总复杂度,不是依赖越少越高级。
61. 为什么不用 Three.js?参考答案
项目已有可复用的 Babylon WebGPU OceanDemo,Babylon 对 Engine、Scene、ComputeShader、PBR、GUI 和资源生命周期的集成适合当前任务。Three.js 生态很强,但切换并不会自动减少复杂度。地理模块则选择 Cesium,因为椭球与测地线是它的专长。
62. 98 MiB 的静态站还能叫性能优化吗?参考答案
不能用“做过优化”掩盖总体积仍大。已有工作解决了首屏竞争、模型压缩、懒加载和运行时负载,但产物预算仍不理想,尤其旧海洋资产。我的结论是体验可用但还有明确性能债,下一阶段会用代码分割、资源去重、KTX2/meshopt 和按设备质量档位继续降低成本。
63. 为什么海洋不直接做视频背景?参考答案
视频实现简单、兼容性和能耗更可控,很多营销站确实应该选视频。项目选择实时海洋是为了让船和波面持续一致、体现实时图形能力并支持场景改造;同时保留静态回退。若真实数据证明互动价值不高,我会把低端设备甚至全量切到高质量视频或图片。
64. iframe 直接读 `contentWindow.scene` 是不是很脆弱?参考答案
是,它依赖同源和子页面内部对象名,属于集成债。当前用它快速隔离版本并控制风险,但长期应该在子页面暴露版本化的 `postMessage` 协议,只传状态和高层命令,不让父页面操作内部 Scene。文档和测试也应锁定协议契约。
65. 为什么没有后端监控和错误上报?参考答案
当前交付范围是静态演示站,仓库里没有接入 RUM 或错误平台,所以我不会声称有线上可观测性。正式发布后至少应采集 Web Vitals、资源失败、WebGPU/Cesium 初始化阶段、设备能力和 JS error,并避免上传隐私数据。
66. 你用了开源代码,项目价值在哪里?参考答案
工程价值不在重复实现 FFT,而在正确选择、理解并整合上游:处理版本隔离、本地运行时、失败降级、真实 ready、视觉对齐、船模压缩、浮力复用、性能与许可证。面试时我会明确上游贡献和自己的改动,并能解释关键原理与边界。
# 十八 行为题与项目复盘
# 面试官问 你在项目中承担什么职责
回答模板
我负责的范围是需求拆分、前端架构、核心视觉模块集成、三语与响应式、浏览器测试和静态部署准备。开源 OceanDemo、Babylon、Cesium 和 React Bits 是第三方基础,我没有把它们包装成自研;我的主要工作是让这些能力在同一产品里具备清晰边界、降级、性能控制和可验证交付。如果实际还有团队成员,应补充视觉稿、内容、素材或代码分别由谁负责。
# 面试官问 你做错过什么
回答模板
早期我曾把“场景对象创建”理解为“用户已经能看到海面”,导致 Loader 退出时露出天空或未完成帧。这个错误让我意识到异步图形系统要定义业务可见的 ready,而不是复用某个技术事件。后来我把 ready 拆成能力、场景、表面 draw call 和浏览器合成四个阶段,并把慢与失败分开。后续如果重做,我会从一开始就设计明确状态机和耗时埋点。
# 面试官问 你如何处理需求变化
回答模板
我先区分内容、布局、视觉参数和底层能力。像课程文案与语言资源集中在 content 模块,路由和页面壳不需要跟着改;3D 场景通过独立组件和 iframe 隔离,调整天空或船模不会扩散到全部页面。变化如果涉及路由数量和 SEO,我会重新评估手写路由与 CSR 是否仍合适,而不是继续往原结构堆特殊判断。
# 面试官问 如何与设计或产品协作
回答模板
我会把主观的“更有科技感”转换成可检查目标:构图、海天线位置、首屏文字对比、动效强度、移动端折行、加载等待和失败状态。每轮保留同视口截图与 QA 记录,先处理遮挡、溢出和交互阻断,再做颜色与氛围微调。对于宣传文案和素材授权,技术实现不能替代事实核验,我会把需要业务确认的项单独列出。
# 面试官问 项目最大的遗憾
回答模板
最大的遗憾是体验完成度高于产物治理完成度。当前仍存在主包大、旧海洋资产重复、单文件接近平台上限、CSR 多语言 SEO 不完整、缺少线上 RUM。这些不是一句“做了懒加载”就能掩盖的。我会优先补新鲜构建基线和性能预算,再做路由拆包、资源格式升级、设备质量分级和可观测性。
# 十九 系统设计扩展题
# 如果要增加报名与支付
不要把表单直接提交到静态页面。应增加后端 API、身份与防滥用、数据库、订单状态机、支付平台 Webhook、幂等键和审计。浏览器创建订单,服务端生成支付请求;支付成功以签名 Webhook 为准,不信任前端跳转结果。个人信息最小化收集并设置保留期限。
# 如果要增加 AI 课程助手
前端只负责对话 UI 和流式展示;后端负责模型密钥、鉴权、限流、Prompt 版本、检索权限、日志脱敏、超时和费用。课程资料进入可追踪的 RAG 索引,回答带来源;工具调用经过服务器权限和参数校验。建立固定评测集,分别评估正确性、引用、安全、延迟和成本。这样才是这个站点从“AI 内容官网”升级到“AI 应用”的真实链路。
# 如果要做百万访问量
静态 HTML、JS 和媒体继续由 CDN;大模型和视频不要和主站产物混在同一发布路径。图片视频使用专门媒体服务和自适应格式。启用 RUM、错误采集和缓存命中率监控。3D 场景按设备、网络与实验分流,并提供低成本版本。报名与 AI API 独立扩展,使用限流、队列和降级,避免营销流量拖垮核心服务。
67. 如果要把当前站点改成 SSR,最先改什么?参考答案
先盘点所有模块顶层的 window、navigator、localStorage 和事件监听,把浏览器依赖移入 Effect 或能力适配层;语言状态改为请求级;路由和 metadata 交给 SSR 框架;3D 组件只在客户端动态加载。SSR 不会让 WebGPU 在服务器运行,它只让首屏 HTML 和 SEO 可预渲染。
68. 如何设计 3D 场景错误监控?参考答案
记录阶段化事件:能力检测、脚本下载、设备创建、Shader 编译、资源加载、首个有效帧、运行时 renderError 和 fallback。附带匿名化的浏览器、GPU capability、耗时和版本,不采集敏感内容。监控 fallback 率、P50/P95 ready 时间和崩溃设备分布。
69. 如何做 3D 功能灰度发布?参考答案
用远程配置或边缘分流给稳定用户 ID 分桶,保留 static、video、realtime 三种策略。先内部和小流量,比较 LCP、INP、错误、fallback、停留与转化,出现异常能立即切回静态,不需要重新部署全部页面。
# 二十 模拟面试
# 第一轮 项目总览
- 用一分钟介绍项目。
- 你的个人职责是什么,哪些是第三方或团队成果?
- 为什么选静态 SPA?
- 五个页面的核心业务链路是什么?
- 项目最难的技术问题是什么?
- 如何证明三语和移动端真的可用?
- 当前最大的技术债是什么?
自检标准:回答中至少出现业务目标、一个核心调用链、一个失败路径、一项验证和一个边界;不要只列技术名。
# 第二轮 前端深挖
- 手写 Router 如何处理前进后退与直接刷新?
- 为什么使用
useSyncExternalStore? - Effect 如何清理第三方运行时?
- StrictMode 下会发生什么?
- 如何避免 stale closure?
- 为什么主包仍然大,如何拆?
- reduced motion 的实现边界是什么?
# 第三轮 图形与性能
- WebGL 和 WebGPU 的核心差别。
- FFT 海洋的完整数据流。
- 为什么 iframe load 不是 ready?
- Cesium 航线为什么用测地线?
- 如何控制 DPR、帧率和不可见页面负载?
- 如何定位显存泄漏?
- 视频背景与实时海洋如何取舍?
# 第四轮 工程与系统设计
- Playwright 覆盖了什么,没有覆盖什么?
- Cloudflare SPA fallback 的原理。
- 如何增加报名、支付和 AI 助手?
- 如何做 SEO 和多语言静态页面?
- 如何做 RUM 和 3D 灰度?
- 依赖原生绑定缺失时怎样处理?
- 如果只能优化一周,优先级是什么?
# 二十一 面试前速记卡
# 必须记住的数字
| 项目事实 | 数字 |
|---|---|
| 正式页面路径 | 5 条 |
| 支持语言 | 3 种:中文、日文、英文 |
| Playwright 测试代码定义 | 34 个用例 |
| 主要视口矩阵 | 1440px 与 390px;历史 QA 还检查 320/768/1024/1440 |
| Cesium 动画上限 | 约 45 FPS |
| 海洋慢加载预览阈值 | 30 秒 |
| Loader 最短展示 | 约 1.2 秒,完成推进约 560ms |
| 当前 dist 文件数 | 845 |
| 当前 dist 总体积 | 约 98 MiB |
| 主 JavaScript 产物 | 约 7 MiB |
| 最大单文件 | 约 23.68 MiB GLB |
| Pages 单文件检查上限 | 25 MiB |
| 压缩后船模 | 约 3.2 MiB,纹理 512×512 |
# 十句核心话术
- 这是 AI 课程品牌官网,不是大模型应用;它证明的是复杂体验层与工程交付能力。
- 复杂 3D 只做增强,核心信息始终由 DOM 提供。
- 海洋 ready 以真实表面绘制为准,不以 iframe load 或固定时间为准。
- 慢和失败是两种状态:慢时预览但继续准备,失败才回退并允许重试。
- iframe 的价值是隔离 Babylon 版本,代价是两套运行时和脆弱的集成接口。
- URL 是可分享意图,localStorage 是设备偏好,所以 URL 优先。
- React 管界面和生命周期,Babylon/Cesium 管每帧,不把 3D 高频状态塞进 React。
- 静态部署降低运维面,但 CSR 多语言 SEO 和真实后端能力有限。
- 测试覆盖常见用户链路,不等于真机 GPU、所有浏览器和生产流量验证。
- 当前最大债务是 Bundle 与资产体积、运行时 SEO和缺少线上可观测性。
# 遇到不会的问题怎样答
不要编造。先复述你确定的项目事实,再明确未知,最后给验证方法。
模板
这个问题我没有在当前项目里做过生产验证,所以不能直接给确定结论。当前实现是……,我能确认的证据是……。如果要验证,我会先用……建立基线,再分别改变……观察……,最后根据数据决定是否采用。这里最大的风险是……。
# 附录 A 代码证据索引
| 主题 | 主要文件 | 可核对内容 |
|---|---|---|
| 应用入口 | src/main.jsx | React 19 createRoot、StrictMode、全局样式 |
| 路由 | src/Router.jsx、src/navigation.js | 五路由、History、链接委托、旧 Hash、metadata |
| 国际化 | src/useLocale.js、src/i18n.js、src/content/messages.json | 优先级、Store、递归翻译、存储异常 |
| 首页 | src/App.jsx | 海洋、Loader、内容和转化入口 |
| 海洋外壳 | src/OceanScene.jsx | iframe 隔离、就绪、相机、船、降级、重试 |
| 海洋就绪 | src/oceanReadiness.js | 表面有效渲染判定 |
| 海天线 | src/oceanHorizon.js | 屏幕位置更新稳定逻辑 |
| 海洋源码 | vendor/OceanDemo/src/scenes、src/ocean | 波谱、FFT、几何、材质、浮力、天空 |
| WebGPU 入口 | vendor/OceanDemo/src/index.ts | WebGPUEngine 和支持检测 |
| 粒子场 | src/AdmissionsParticles.jsx | Babylon 9、图标纹理、相机和清理 |
| Cesium | src/CareerGlobe.jsx | 动态加载、测地线、按需渲染、销毁 |
| 动画 | src/reactbits/SiteMotion.jsx、ScrollFloat.jsx、BlurText.jsx | GSAP、Motion、键盘与 reduced motion |
| 翻页书 | src/AIEvolutionBook.jsx | PageFlip 与页码状态 |
| 海报 | src/CoursePoster.jsx | dialog、三语资源、下载、键盘 |
| 构建 | vite.config.js | WGSL、Cesium copy、3D 资源类型 |
| 测试 | playwright.config.js、tests/browser/i18n.spec.js | 浏览器矩阵和异常路径 |
| 发布检查 | scripts/check-pages.mjs、public/_redirects | Pages 限制、关键资产、SPA fallback |
| 部署说明 | DEPLOYMENT.md | Cloudflare 流程、语言与域名边界 |
| 设计与响应式证据 | design-qa.md、responsive-qa.md | 多轮视觉、交互和视口验证 |
| 文案风险 | ad-copy-review.md | 已调整和仍需事实核验的内容 |
# 附录 B 下一步学习顺序
- 亲自画出 React、iframe Ocean、Babylon 粒子和 Cesium 四个运行时的生命周期。
- 能不看文档讲清频域、二维 IFFT、ping-pong 纹理和 Compute Shader dispatch。
- 用 Chrome Performance、Network 和 Memory 对三条重场景各做一次录制。
- 修复依赖安装后重新执行 build、lint、Playwright,保存新鲜结果。
- 给路由和 3D 页面做动态 import,比较前后主包与首屏指标。
- 设计一个真正的 AI 课程助手后端,用另一个项目证明模型、RAG、权限、评测与成本控制。
最后提醒:面试官真正想确认的不是你记住多少名词,而是你是否知道每个技术解决了什么问题、失败时怎样恢复、证据在哪里、哪些成果是自己的,以及哪些边界仍然存在。能诚实地把这些讲清楚,比把项目包装成“全栈全能、完全自研、生产级高并发”更有说服力。