如果你今天打开一个内容型网站的首页,把浏览器切到“禁用 JavaScript”的状态,再用手机流量这种弱网环境访问一次,多半会看到一个半死不活的白屏,或者一个转了几秒还在打转的 loading 动画。很多人已经默认这就是现代 Web 的宿命,觉得“没有 JS 的 Web 没意义”。可恰恰是这个默认观念,正在让太多前端架构往同一个偏方向上狂奔。
行业里判断一个前端工程师的水平,常常看他能把多少复杂的交互做得流畅。但很少人反过来问:这个页面里有多少交互是用户真正需要的?有多少状态管理是为了管理一个本来可以由服务器输出的页面而存在的?有多少副作用是框架强加给这个项目的?当一个团队在“发散创新”的旗帜下各自发挥——有人用组件化框架写页面,有人单独为一个小按钮引入交互方案,有人搭建一套完整的私有状态层——页面就在这些局部创新里越滚越大,最终 JavaScript 不再是“增强体验的脚本”,而是架构本身。
这篇文章想聊的,不是“消灭 JavaScript”,也不是给某一种框架定生死。我想拆开“过度依赖 JavaScript”这件事的完整账本,再聊几条可以落地的破局路线。如果你正被首屏性能、构建速度、团队协作成本折磨过,也许这里面的某些思路能用得上。
1. 先承认一件事:JavaScript 依赖不是“用得多”,而是“越了界”
1.1 从增强脚本到宿主应用:一次失控的渐变
早期的 Web 里,JavaScript 的位置其实很边缘:表单校验、图片滚动、弹窗提示,都是些“锦上添花”的事。页面完整内容先由服务器输出,脚本只是在 DOMContentLoaded 之后动一动某些节点。
后来组件化框架改变的不仅是写法。它们把整个页面挂在一个<div id="app">上,让“页面内容”变成了 JavaScript 运行时的产物。这个变化本身没有对错,但它挪动了一个关键边界:内容的渲染职责从服务器转到了浏览器脚本。
于是出现了很多在今天看很常见的组合:一个只有三千字介绍文案的营销页,加载了大约 600KB 的 JavaScript 跑到客户端拼页面;一个面向内部员工的信息查询列表,为了几个筛选项和分页功能,搭建了一个拥有完整 Router、状态层、请求层、UI 组件库的“小型操作系统”。这些工程体量当然有它们存在的理由,但更多时候,理由并不充分。
1.2 发散创新是怎么成为债务加速器的
“发散创新”不是贬义词。它指的是团队在既定方案之外探索局部最优解的冲动:为了让某张图表更快,有人绕过组件库手写操作;为了让某个详情页秒开,有人把接口数据预编译进了静态文件;为了让中后台表单更好用,有人引入了第二套状态管理方案。
如果每一条创新最后都能收敛回统一的架构约定里,那这些分散的尝试就是有价值的实验。但实际项目中,发散创新往往缺一个收敛步骤。每个小组的局部最优解叠加起来,反而形成了一张越来越臃肿的依赖图。我在很多项目里都能看到类似的痕迹:某个包全项目只剩两个组件在用,但因为迁移成本高,谁都不敢删;某个全局状态库只服务于一个页面的一个弹窗,但它被放进每个模块的初始化流程里。
这让我越来越相信,前端架构最大的风险不是技术选型错误,而是缺少一个“这个功能到底值不值得用 JavaScript 实现”的判断机制。
1.3 重新划定职责边界
要判断“越界”,可以做一个简单的职责划分:内容渲染、路由导航、表单交互、样式呈现、状态保持,哪些是本该由服务器输出的?哪些是浏览器原生能力可以做到的?哪些是脚本介入后收益远大于成本的?
我自己的经验是先把问题倒过来,列一份《必须使用 JavaScript 清单》:
- 需要实时双向同步的复杂交互(比如协作编辑)
- 需要高频更新的可视化视图
- 需要客户端持久化或者离线能力的场景
- 需要访问设备 API 的场景
- 需要前端做复杂校验和中间态管理的表单
剩下的东西,包括文章内容、列表结构、首屏样式、基本导航、详情展示,几乎都可以靠服务端输出配合 CSS 搞定。把这条线画清楚之后,“过度依赖”的程度立刻就能暴露出来。
2. 过度依赖 JavaScript 的七本账,每一笔都会在某个时间点结清
2.1 首屏性能税:最沉重的隐形开销
首屏渲染是 JavaScript 依赖最直接的成本。用户打开页面的那一瞬间,需要先下载脚本,再解析、编译、执行,之后才能把界面画出来。在 4G 到 5G 切换的移动网络环境里,这一段延迟是看得见摸得着的,尤其在低端安卓机上,运行时的解析执行时间往往比下载时间更致命。
有一个直观的类比:如果页面内容是一段写在 HTML 里的文字,那它就像直邮到户的信件,打开信封就能看到;如果页面内容必须通过 JavaScript 渲染,那它就像需要你先组装一台打印机,再由打印机把信打出来。组建打印机本身没有错,但如果只是为了看一行文字,那这笔开销明显不合理。
解决方向也不是一刀切。对内容密集型页面,优先让服务器把 HTML 发出来;对真正复杂的应用,则要把加载过程拆成“必要加载”和“按需加载”两部分,让核心路径上的脚本量尽量小。
2.2 可靠性折损:一个异常就能带走整屏内容
当整个页面都由 JavaScript 产出时,可靠性就绑在了脚本执行是否成功上。无论是一个未被捕获的运行时异常,还是某个接口返回了无法处理的数据结构,结果都可能是白屏、空白区块、无限 loading,或者状态错乱。
脚本异常的范围比想象中大。第三方 SDK 报错、浏览器升级后某个 API 变更、用户安装了某种屏蔽脚本的扩展、公司内部监控脚本抢占了全局变量……这些都是极常见的事。原生 HTML 在这类问题上要可靠得多,因为浏览器负责把内容画出来,脚本只是附加动作。
2.3 SEO 与内容可达性:爬虫不买账
搜索引擎爬虫和各类内容抓取工具,在 JavaScript 执行上一直持保守态度。就算现代搜索引擎对客户端渲染的容忍度提高了,“能抓”和“抓得好”也不是一回事。内容型网站如果把整份文章正文都交给前端脚本渲染,页面收录质量、索引速度都可能出现明显波动。
即使你不在乎搜索流量,内容可达性仍然重要。很多企业内部的文档平台、数据分析平台,分享链接时希望别人打开就能看到概要,但对方环境里可能禁用脚本,或者在公司内网安全策略下脚本无法完全执行。服务端直接输出内容的页面,在这类场景里就是天然更强。
2.4 可访问性裂缝:被忽略的用户群
屏幕阅读器和键盘导航对动态渲染内容的支持,远比直接存在于 HTML 中的内容要薄弱。当页面结构靠 JavaScript 在运行时创建,辅助技术需要等待脚本执行完成,还要依赖框架生成正确的可访问性语义(ARIA 属性、焦点管理、语义标签)。
说得直白一点:用原生<button>和一个可访问性语义缺失的<div role="button">对比,后者的键盘聚焦、朗读、表单提交行为都更难做得正确。过度依赖框架的组件抽象,很容易让这些基础语义在层层封装中被稀释。
2.5 维护成本:依赖树不是树,是热带雨林
依赖膨胀带来的维护压力,通常不会出现在项目初期,而出现在一年之后。某个 UI 组件库升级主版本,脚手架构建报错 200 条;某个工具包被两个模块间接依赖,版本冲突让测试环境行为诡异;甚至只是删一行全局配置,都有可能牵连一系列不知道从哪里延伸出来的副作用。
传统多页应用时代,页面之间是松耦合的,一个页面的依赖故障不会波及其他页面。而在大型单页应用里,全局运行时把所有模块捆在一起,任何局部问题都可能被放大成架构问题。
2.6 团队认知负荷:上下文切换正在吃掉效率
现代前端团队的开发过程,往往不是围绕业务逻辑展开,而是围绕框架机制展开。状态更新的触发规则、依赖收集的时机、组件生命周期的执行顺序、路由懒加载的边界……每一个都是需要额外学习的“隐式规范”。
老团队成员占优势,但新人上手成本显著提高。一个页面出现展示异常,调试链路可能是:查数据源 → 查状态更新 → 查组件重渲染时机 → 查样式优先级,绕了一大圈才发现只是 CSS 里少了条规则。这种上下文切换对研发效率的损耗,在项目复杂到一定程度后会变得非常明显。
2.7 架构刚性:框架模式反过来限制了优化
组件化框架带来的编程模型,本身有极强的约定性。它适合大多数场景,但也让团队很难在局部做极端优化。比如,某个只读页面根本不关心状态变化,但为了统一初始化流程,它也会被套进同一套响应式体系中;某个静态区块完全不需要水合,但为了和其他页面保持同构,它还是会被打包进脚本。
这种刚性不一定是框架的问题,而是团队把自己的架构完全锁死在框架推荐模式里之后,自然丧失了对局部的弹性控制。
3. 破局的第一性原理:HTML 与 CSS 本来就是一整套被低估的运行时
3.1 把 HTML/CSS 当运行时,而不是“模板”
一提到“减少 JavaScript”,很多人的第一反应是:那交互怎么做?但其实近几代浏览器原生的能力已经大幅扩张,很多过去必须写脚本的交互,现在原生就能完成。
折叠面板可以用<details>和<summary>,弹窗可以用原生<dialog>,浮层提示可以用popover属性,表单校验可以用原生required、pattern、:invalid伪类,选项卡切换、响应式布局、自适应字号,都有对应的 CSS 方案。更别说 CSS Grid、Flexbox、容器查询、:has()这些能力,让过去需要 JavaScript 来计算位置、判断状态、动态切换类名的场景大幅减少。
这背后的价值不止是省代码。原生能力是浏览器内置的,行为一致性最强,可访问性也有保障。你要做的只是把这些能力以合理的方式组合进页面。
3.2 一个“快速筛选器”帮团队判断到底要不要 JS
我在团队里用过一套很简单的筛选方法,帮助大家判断一个功能到底要不要引入脚本:
- 这个功能在页面初次加载时必须立即出现吗?
- 它需要和服务端数据实时同步吗?
- 它必须依赖用户输入或设备状态吗?
- 它的视觉效果能否完全由 CSS 状态切换完成?
如果四个问题都是否定的,那它就很可能不需要 JavaScript。举个例子,一个产品展示页的轮播图,如果只是每隔几秒自动换一批图片,那完全可以用纯 CSS 的滚动吸附和动画实现,不需要为一个轮播组件挂载一整套交互状态。
3.3 “渐进增强”不是老古董,而是性能预算的前置
渐进增强的思路放到今天反而更清晰:先把内容以最基础、最可靠的形式交给用户,再根据浏览器能力逐层升级体验。
具体到工程实现,就是让服务端输出的 HTML 保持完整可用,然后再通过脚本给页面“加点料”。这样的页面,即使在弱网、脚本阻塞、用户禁用了 JS 的环境下,核心内容依然可用;而在现代浏览器里,脚本加载完毕后会无缝地注入更丰富的交互。
这么做看起来和现代框架的默认路径是反的,但它带来一个非常务实的好处:你把性能和可用的底线锚定在了一个极低的位置上。后续所有优化都只是在这个底线上做加法,而不是在一条脆弱的主干上修修补补。
4. 四条值得认真考虑的破局路线:SSR 边界、岛屿架构、Web Components 与标准优先
4.1 路线一:把渲染主权还给服务器
服务端渲染(SSR)不是新鲜概念,但很多项目里的 SSR 其实走歪了:它在服务端生成 HTML 之后,依然会在客户端把整个组件树再跑一遍,水合过程加载的体积并不比纯客户端渲染小多少。
更健康的方式是“部分 SSR”,或者说“只在必要的地方水合”。把不需要交互的页面区块直接作为静态 HTML 输出,不参与客户端水合;只有少数需要响应式状态的区块才生成对应的交互脚本。这样,首屏脚本体积可以明显下降,也不会牺牲核心交互体验。
4.2 路线二:岛屿架构
“岛屿架构”可以看作部分 SSR 的进一步抽象。整个页面被看成一个静态 HTML 的海洋,海洋里分布着一座座独立的交互岛。每一座岛都有自己独立加载的脚本,岛与岛之间互不依赖。
这种架构对内容型页面特别合适。比如一个长文章页,文章正文是静态 HTML,评论区是一套交互组件,顶部工具栏又是一套交互组件。两套组件各自加载自己的脚本,互不阻塞,哪怕评论区的脚本挂了,文章照样能读。实现思路也很直接:服务端负责输出静态骨架,每个动态区块用自定义元素或者独立脚本来增强自身行为。
4.3 路线三:原生自定义元素替代运行时
Web Components 的标准能力容易被忽视,但它其实很出色:自定义元素可以用原生类实现,影子 DOM 提供了样式隔离,自定义事件可以跨组件通信,不用引入任何运行时。
如果团队对框架没有硬性统一要求,完全可以把公共组件库改造成自定义元素实现。它的跨框架复用能力很强,同一个组件可以同时被不同技术栈的页面引用,不会出现“为了用这个组件,必须把整个框架引进来”的尴尬。
4.4 路线四:先标准,后增强
这不算具体技术路线,而是一种工程纪律:实现任何一个功能前,先查一遍浏览器原生能力;原生方案能覆盖,就直接用;覆盖不了,再考虑轻量脚本;脚本复杂度再失控,才考虑引入重型框架或状态层。
这条路线的收益不是某个具体指标,而是帮团队形成了一种肌肉记忆。时间久了,你会越来越少地“因为框架存在,所以用框架实现”,而是越来越倾向于“因为确实需要,才引入额外依赖”。
4.5 选型参考比较
| 破局路线 | 适用场景 | JS 用量降幅 | 交互能力 | 实施成本 |
|---|---|---|---|---|
| 服务端渲染 | 内容站、营销页、文档站 | 高 | 中 | 中 |
| 岛屿架构 | 长文章加局部复杂交互 | 中高 | 中高 | 中高 |
| Web Components | 跨框架组件库、中后台 | 中低 | 高 | 高 |
| 标准优先 | 几乎所有前端项目 | 视场景而定 | 中 | 低 |
我这几年做选型,越来越不喜欢“一步到位”的决定。更靠谱的做法是,先让团队把“标准优先”作为默认策略,遇到具体场景再评估是否需要上 SSR 或者岛屿架构。
5. 一次真实项目瘦身全过程:从“全是 JS”到“JS 只在需要的地方出现”
5.1 项目背景与问题定位
我接手过一个内部管理系统,主要功能是给业务人员展示投放数据并提供基础筛选。系统启动时,构建产物接近 3MB,首屏完全渲染在中等配置的开发机上大概要 4 秒左右。生产环境更差,经常出现白屏报错,原因基本都集中在某个第三方组件异常。
当时团队的第一反应是“压代码”:拆包、开启 Gzip、用流式加载,效果确实有,但改动一轮之后,性能预算依然不够理想。真正的转折点来自一次“功能与脚本量清单”盘点:我们把系统里每个页面的功能列出来,然后问一个问题——如果现在完全不写 JavaScript,这些功能哪些会消失?
结果发现,真正依赖脚本的只有:筛选联动、表格动态排序、详情弹层和一段图表渲染。其余绝大部分页面结构、导航、静态信息展示、表单基础布局,竟然靠原生 HTML 和 CSS 就能实现。
5.2 先做出《必须保留脚本》的清单
这是我们第一份产出,直接决定了重构范围:
- 筛选联动:需要实时请求接口并更新表格,必须脚本。
- 表格排序:前端排序和后端排序都可以,如果切到后端排序,前端脚本可以删掉一大半。
- 详情弹层:用原生
<dialog>实现,只需要一个很小的脚本封装,不需要大状态层。 - 图表渲染:需要脚本,但可以单独拆包,不进入首屏主路径。
其余所有列表页、详情页、配置页,统一改为服务端直接输出完整 HTML。这么做的好处立竿见影:每个页面的首屏不再依赖“应用启动”,而是直接展示内容。
5.3 按步骤替换,不要一次推翻
我们没有直接删掉框架重写,而是分成了四步:
第一步,把几个纯展示页面从组件化渲染改成服务端模板输出。这些页面原本就没有太强交互,改造成本低,但收益最明显。
第二步,把弹层、下拉筛选这类轻交互替换成原生元素和少量脚本。团队里专门排了一个同事负责把零散的 UI 组件库依赖收敛掉。
第三步,把表格排序切换到后端排序。前端只保留一个“点击表头触发请求”的事件绑定,几十行代码完成,省掉了前端对整张表的数据再处理。
第四步,把图表、审计日志这类重模块拆成异步加载的独立 chunk,主页面完全不再引用它们。
重构后的首屏脚本量从约 900KB 降到约 180KB,主要来自一部分残留组件和图表逻辑。首屏时间从 4 秒左右降到 1.2 秒上下,白屏报错也大幅减少。
5.4 一个容易翻车的细节:状态同步
重构过程中踩过最典型的坑是:服务端输出的 HTML 和前端脚本各自维护一份状态。举例来说,筛选区选中了某个条件后,重新渲染的表格如果由前端脚本生成,那表格里“当前筛选条件”的展示就会和服务端 HTML 里“当前筛选条件”的展示不一致。
后来我们定了一条规则:页面状态优先放在 URL 查询参数里。服务端读取 URL 参数输出对应结果,脚本只负责替换 URL 参数并触发页面整体刷新。这回到了最传统的“服务端渲染 + URL 状态”模型,看起来不够“现代”,但在当前场景下,它比在内存里维护两份状态可靠得多。
这次重构给我最大的启发不是“技术路线要选对”,而是决策顺序要反过来:先搞清楚功能边界,再决定用多少脚本,最后才选具体实现方案。把顺序反过来,几乎总会陷入“先有工具,再为工具找用例”的怪圈。
6. 破局落地时真正难的四个边界问题
6.1 新旧架构共存:不要停下来重写
很多团队听到“降低 JavaScript 依赖”,第一反应是找一个大项目做整体迁移。但这恰恰是风险最大的动作。
更稳妥的做法是把新页面当作“标准优先”的试验田,同时把存量页面的高频问题挑出来单独优化。比如,哪个页面首屏最慢,就先把它的静态部分抽出来做服务端输出;哪个页面状态层最复杂,就先去检查哪些状态其实可以外置到 URL 参数。新旧架构长期共存不是临时的妥协,反而是一种有意的策略,它能让你在每一步都可以回滚,不被一次性大重构绑架。
6.2 用性能预算守住底线
所谓性能预算,就是给每个页面立一个“脚本量上限”。比如定义一个指标:首屏页面在手机上加载的 JavaScript 总字节数不能超过某个阈值,一旦超过,构建流程直接报警。
这个预算机制比口头约定有用得多。它让“减少 JavaScript 依赖”不再是一种抽象理念,而是每个合并请求里可以被客观检验的数字。数据不达标,代码就进不了主干。
6.3 团队的思维升维与技术基建
过度依赖 JavaScript 从某种意义上也是团队技能的“舒适区陷阱”。大多数前端工程师的训练路径都是从框架入场,很少有人系统地学习过浏览器原生能力、CSS 新特性、服务端渲染的取舍。要让团队形成新的判断力,得靠两件事:一是定期分享原生能力的最新进展,二是建立公共组件库,把“标准优先”的成果沉淀成复用资产,而不是让每个工程师都从头摸索。
6.4 避免从一个极端掉进另一个极端
降 JavaScript 依赖,不等于逢 JS 必反。如果一个功能场景确实需要复杂前端交互,那它就是应该用脚本;如果只是为了让“代码更少”而强行用服务端模板硬造一个体验很差的交互,那属于南辕北辙。
我总结成一句话:降低过度依赖,是为了让 JavaScript 回归“增强”的本位,而不是把整个体系带进新的苦修主义里。
我在自己的项目里复盘时发现,真正扭转局面的往往不是某个具体框架或工具,而是一套贯穿所有决策的“成本感知”。什么时候该让服务端多干一点,什么时候该让脚本介入,判断依据始终是同一个:这一步到底给用户创造了多少可感知的价值,又让架构背负了多少新的复杂度。把这个感知建起来,JavaScript 使用量会自然而然回到它该在的位置上。