路由:我不够聪明,无法实现 SPA
在第 2 部分中,我写了很多内容……
我认为这需要一个 MPA。(又名传统的 Web 应用程序。站点。Thang。非 SPA。无论如何。)
好吧,但我为什么决定这么做呢?为了演示速度最快的 Kroger.com,我应该考虑所有选项——为什么不做一个单页应用呢?
好吧,我们来试试SPA吧
虽然React 大约大了两倍,但我肯定能在 20kB 的预算内塞进一个 SPA 应用。我见过有人这么做:PROXX游戏的初始加载大小正好在 20kB 以内,而且在比 Poblano更弱的手机上运行也很流畅。
因此,我粗略地估计一下……
- HTML 除了
<script>
s 之外几乎没有其他内容,因此其大小可以忽略不计。 - 剩下的 5.4kB 对于一些手动调整的 CSS 来说已经足够了。
- 这个总数是悲观的:Bundlephobia 的估计指出“如果只使用软件包的部分内容或者软件包共享共同的依赖关系,实际大小可能会更小。”
看起来可行。
……但这真的是SPA 所需的全部代码吗?
我知道我最终会需要一些代码:
- UI 元素和 API 请求/响应格式之间的转换
- 更新
<head>
- 不使用全尺寸图像链接,而是使用灯箱脚本来避免卸载 SPA
- 检查并重新加载过时的 SPA 版本
- 代码拆分以适应 20kB 的首次加载(我的组件估计仅适用于主页),这意味着动态资源加载代码
- 重新实现
beforeunload
未保存用户输入的警告 - 分析需要为 SPA 提供额外的代码。请考虑两个与我们的网站特别相关的示例:
但是我没有具体的数字来支持这些——在解决这些问题之前我放弃了 SPA 方法。
为什么?
并不总是与性能有关
我不想演示一个仅仅因为忽略了真实网站职责而速度很快的玩具网站。对我来说,(杂货电商的)这些职责是……
- 🛡安全性甚至高于可访问性
- 降级旧浏览器的 HTTPS 密码不值得让信用卡被盗。
-
您必须先保护客户信任的数据,然后才能使用它。
- ♿无障碍设施,超越速度
- 仅为有能力的人提供的快速网站会造成不平等,而为所有人提供的较慢网站则会创造平等。
-
请注意,速度是可访问性的两倍。
- 🏎️速度甚至超过光滑度
- 愉悦需要准时出现。
- 用户认为速度快的网站更易于使用、设计更精良、更值得信赖、更令人愉快。
因此,我拒绝在安全性和可访问性上妥协。如果加速与这两者相冲突,那对我来说就毫无意义。
安全
MPA 和 SPA 存在大多数相同的安全问题;它们都关注XSS/CSRF/其他字母组合攻击,最终由一些代码执行防御。有趣的区别在于这些代码的存放位置以及这对持续维护意味着什么。
(除非 SPA 端点的无状态理想导致类似 JWT 的东西,在这种情况下你会遇到更严重的问题。)
安全代码所在位置
采取 CSRF 保护:在 MPA 中,它在浏览器中表现为<input type=hidden>
s<form>
和/或samesite=strict
cookie;这在网站的正常 HTTP 生命周期中增加了一小笔开销。
SPA 具有相同的开销1,并且......
- JS 获取并刷新反 CSRF 令牌
- JS 检查哪些请求需要这些令牌,然后附加它们
- JS 处理受保护响应中的问题;以某种方式修复或向用户显示它们
重复进行身份验证、转义、会话撤销以及强大、安全的应用程序的所有其他细节部分。
此外,你的安全机制越深入、越实用,SPA 方法对所有用户的惩罚就越大。那个罕见但至关重要的警告,提醒你立即联系客服?它(或动态加载它的代码)可以存在于每个人的 SPA 包中,或者仅在压力情况发生时才给 MPA 的 HTML 造成负担。
这对安全维护意味着什么
对于多个公开的 API,您必须对每个 API 进行渗透测试/模糊测试/监控等。理论上,这与对POST
MPA 中每个可用 URL 进行同样的操作没什么不同,但是……
当然,9 支队伍中有 8 支都积极更新了那个存在漏洞的输入解析库。可惜的是,第 9 队的高级开发人员本周缺席,初级开发人员则陷入依赖冲突的泥潭,而现在,一个冰岛青少年黑客团伙威胁要公布所有同时购买泻药和蛋糕粉的人的记录。2
这个问题通常用统一网关来解决,这会导致 SPA 产生请求链,并增加脚本 KB 来连接它。但对于 MPA 来说,它们本身就是统一网关。
无障碍设施
与安全性不同,SPA 存在其独有的可访问性问题。(任何客户端路由也存在同样的问题,例如Hotwire 的 Turbo Drive。)
我需要代码来恢复标准页面导航可访问性:
- 记住并恢复导航中的滚动位置。
- 将上次使用的元素集中在后退导航上(考虑到
autofocus
、、tabindex
JS 驱动.focus()
、其他复杂因素……) - 将页面代表元素聚焦于向前导航,即使它通常不可聚焦。这充满了陷阱。
在正确处理客户端路由的其他困难部分的同时,完成所有这些工作:
- 后退/前进按钮
- 超时、重试和错误处理
- 用户取消和双击
- API 脚枪的历史——查看其拟议替代品的理由
- 仅当链接是同源的、不是页内的
#fragment
、相同的target
、需要身份验证时才使用 SPA 导航... - 检查Ctrl/ ⌘/ Alt/ Shift/ 右键/中键单击、在新选项卡/窗口中打开的键盘快捷键,或辅助技术配置的非标准快捷键(祝你好运)
- 重新实现浏览器的加载/错误 UI,你的 UI 就不会那么好:它们可以报告代理配置错误、DNS 故障、它们正在等待网络堆栈的哪个部分等。
从理论上讲,社区图书馆应该有助于避免这些问题……
React 和其他 SPA 框架的大多数路由器都开箱即用。这个问题至少在五年前就已经解决了。网站必须特意解决这个问题。
我们在工作中使用这些库,但我要告诉你:我们仍然经常会不小心把它们搞乱。我问了一位流行的 React 路由器的维护者,他的看法是:
我们可以告诉你在 UI 中何时何地应该聚焦某个元素,但我们不会替你聚焦任何东西。你还需要考虑滚动位置是否混乱,这非常依赖于应用本身,并且取决于屏幕尺寸、UI、标题周围的填充等等。抽象和概括起来非常困难。
更糟糕的是,一些 SPA 可访问性问题目前无法修复:
例如,屏幕阅读器可以在加载新页面时生成其摘要,但无法使用 JavaScript 触发该功能。
另外,还记得速度如何使可访问性加倍吗?
为了提高可访问性和性能,您应该始终限制昂贵的查找和操作,但是尤其是在页面加载时。
我强调的是——如果您应该避免在页面加载期间进行大量处理,那么 SPA 有一个明显的缺点。
我需要一些东西来分解文本。
假设我把这些都做到了。当然,这听起来很困难,而且容易出错,但理论上是可以做到的……只要添加客户端 JS 代码就行。这样一来,我们又回到了最初的问题。
有时这与性能有关
你可能没有我那样的 20kB 预算,但30-50kB 的预算可不是我发明的。记住:任何添加到页面上的内容只会让页面变慢。
除了预算限制之外,看似很小的 JS 下载在廉价设备上也具有真实的、可衡量的成本:
代码 | 启动延迟 |
---|---|
反应 | ~163毫秒 |
预行动 | ~43毫秒 |
裸事件监听器 | ~7毫秒 |
详情请见 Jeremy 的后续文章。请注意,这些数据来自诺基亚 2,它比我的目标设备性能更强大。
我看到你那条“过早优化”的评论
不必担心前端框架的通常建议如下:
- 流行的框架为一些速度很快的网站提供支持,因此哪一个并不重要 - 它们都可以很快。
- 一旦遇到性能问题,您的框架就会提供优化它的方法。
- 不要避免使用库、模式或 API,除非它们会导致问题 — — 否则您就会过早进行优化。
“过早优化”的想法一直比这更微妙,但这种逻辑似乎是合理的。
然而……如果你添加一个库来加快后续更新速度,却以降低首次加载速度为代价……这难道不已经是需要优化的方向了吗?按照这个逻辑,只有当你证明 MPA 方法对你来说太慢时,你才应该选择 SPA。
即使 10 毫秒的高内存峰值也可能导致 ZRAM 启动(超级慢),甚至导致应用程序崩溃。发送到 P99 网站的 JS 数量可不是个好消息。
ZRAM 的影响是系统范围的。由于页面使用过多,键盘可能无法快速显示。
更少的代码可以使一切变得更快。
— 亚历克斯·拉塞尔
我在制作演示时打开了性能开发工具,并对自己的代码进行了内部测试。每次测试更重的 JS 方法时,我都能得到确凿的证据,证明避免使用这种方法并非过早优化。
不仅仅是捆绑包大小
除了客户端 JavaScript 的不可阻挡的吸引力之外,SPA 还有其他不明显的性能缺点。
-
页面内更新会缓冲Web 的流式传输,导致JSON 的渲染速度比 HTML 慢,因为缺乏增量渲染。即使您没有刻意进行流式传输,浏览器也会增量地渲染从网络流入的字节。
-
内存泄漏不可避免,但在 MPA 中却很少造成影响。在 SPA 中,一个团队的内存泄漏会毁掉整个会话。
-
JS 和
fetch()
网络优先级低于“主要资源”(<a>
和<form>
)。这甚至会影响操作系统如何优先处理你的应用。 -
最后,也是最重要的一点:服务器代码可以进行测量、扩展和优化,直到您确信可以不再担心性能问题。但客户端却糟糕得无可救药,搭载着十年前的芯片和新款手机里吝啬的内存。Web 的规模和多样性使得客户端“足够快”无法判断。而且,如果您的使用统计数据来自 JS 驱动的分析工具,而这些工具必须下载、解析和上传才能记录用户……您能确定低端使用情况吗?
新鲜负载比你想象的要多
核心 SPA 权衡:第一次加载速度较慢,但它设置了额外的代码以使未来的交互更快。
但在某些情况下,我们无法控制新的负担何时发生,这使得一次性付款更像是复合债务:
- 页面冻结/驱逐
- 移动浏览器会主动将页面置于后台、冻结并丢弃。
- 切换应用程序/标签时经常会令人烦恼地卸载第一个应用程序/标签——问问任何 iOS 用户。
- 浏览器/操作系统更新和崩溃
- 许多设备在充电时会自动更新。
- 我相信您可以猜出 SPA 还是 MPA 崩溃得更频繁。
- 深层链接和新标签页
- 无论我们是否喜欢,用户都会在新标签页中打开内容。
- 外部链接必须重新启动,削弱电子邮件/广告活动、搜索引擎访问和共享链接。
- 多设备使用确实没有帮助。
- 应用内浏览器几乎不与默认浏览器缓存共享任何内容,这将用户认为的回访转变为新的加载→解析→执行。
- 有意刷新页面
- 用户经常会根据实际/感知到的问题进行刷新,尤其是在支持期间。全世界都会考虑Refresh ↻“修复”按钮,而且他们并没有错。
- SPA 经常会自我刷新以进行登录、错误处理等。
- 一个相关的问题:你知道那些“此网站已更新,请刷新”的消息吗?它们会打扰用户,触发刷新,并且会复制一些没人喜欢的原生应用的部分功能。
但是当新页面加载速度很快时,您可以作弊:当它几乎是即时的时,谁会在乎重新加载?
SPA 更加脆弱
虽然每次导航都“重新启动”看起来很浪费,但这可能是我们网络最好的生存机制:
这些错误很大程度上是由于用户运行奇怪的小众或过时的浏览器,其中 Javascript 或 DOM 实现损坏,用户使用有缺陷的浏览器扩展将自己注入到您的范围中,广告拦截器阻止您的某个
<script>
标签,浏览器缓存或中间盒损坏,以及其他更奇怪、更奇特的故障模式。
典型的 Datadog 错误摘要。
考虑到大多数错误都是暂时的3,当遇到错误时,只需重新启动进程,使其回到已知稳定的状态,可能是一个非常好的策略。
3 根据 Jim Gray 在《为什么计算机会停止运行以及我们能做些什么?》中的说法,132 个错误中有 131 个是暂时性错误(它们是不确定的,当你查看它们时它们就会消失,再次尝试可能会完全解决问题)。
-
我们控制服务器的功能和已知错误,并且可以比浏览器更彻底地监控它们。
-
客户端错误监控必须下载、运行和上传才能记录,这更加不稳定,并且增加了其自身的有趣缺点。
-
依赖客户端 JS 是一个已知的未知问题:
- JavaScript 并不总是可用的,这不是用户的错。
- 为什么可用性很重要
- Dan Abramov 对 React 的警告源于
<body>
。 - 用户代理干预是指浏览器故意干扰你的 JS。
总的来说,SPA 对客户端 JS 的依赖导致它们在各种情况下都可能出现不可预测的失败:我们无法控制的地方,我们未曾规划的场景。足够多的极端情况加在一起,就是人性的极限。
SPA 何时是不错的选择
还记得之前的PROXX吗?它确实只有 20kB,但它也不用担心很多事情:
- 其内容是程序生成的,仅需几个简单的交互。
- 它的视图很少,只有一个 URL,并且不需要从服务器加载进一步的数据。
- 它不关心安全性:这是一款无需登录或多人游戏的游戏。
- 如果它无法访问3或因客户端漏洞而崩溃……那又怎样?这只是一场游戏。没有人会错过有用的信息或服务。
PROXX 作为 SPA 非常完美。你可以用它制作一个扫雷游戏的克隆版本,<form>
并附带一个服务器,但可能感觉没那么好玩。游戏通常应该以牺牲其他品质为代价来最大化乐趣。
同样,Squoosh SPA也合情合理:上传未优化图片的开销可能超过了昂贵的客户端处理开销,再加上离线和隐私优势。但即便如此,市面上也有很多服务器端图片处理器,例如ezgif或ImageOptim online,因此显然存在细微差别。
你不必走极端!如果合理的话,你可以将 JS 密集型交互隔离到各个页面:SPA 可以轻松嵌入到 MPA 中。(不过反过来……如果真的可以的话,听起来它会继承两者的弱点,而没有它们的任何优势。)
但是如果 SPA 仅带来☹️,它们为什么存在呢?
我们终于看到钟摆摆向远离SPA的境地,也许有一天你会有选择的余地。一方面,我很高兴能为你提供更多相关文献:
另一方面…
请击败离线优先的 ServiceWorker 缓存应用程序外壳,甚至是位于地球另一端的本地 CDN 上的静态 HTML+JS 和 cgi 页面。
— 米哈伊尔·马洛
确实如此。仅仅说“不要使用客户端导航”是不够的——这些对于任何网站都很重要,无论是 MPA 还是 SPA:
- 离线优先的可靠性和速度
- 尽可能靠近最终用户提供服务
- 不使用 CGI(现在是 2022 年了!至少使用 FastCGI)
那么,下一次:我们能否获得 SPA 所享有的好处,而又不遭受它们极不享受的后果?
-
有人说,SPA 可能只需要从子域名获取数据来保证
origin
标头的有效性,而无需使用 CSRF 令牌。或许如此,但新增的 DNS 查找本身就对性能有负担,而且比你想象的要频繁得多。↩ -
不,这不是我们遇到的事件。首先,这些青少年是卢森堡人 。↩
-
PROXX 确实在可访问性方面投入了大量精力,这很酷 。↩