原文: Frontend Performance Love Story - Tobias Uhlig
一个以空 body 开始的网站永远无法获得好的 Lighthouse 评分!
我非常高兴能为大家介绍一款性能卓越的应用程序。这不仅体现在初始化渲染上,更体现在导航上。如果你对前端工程非常感兴趣,我强烈建议你仔细阅读这篇文章。
1.简介
在我上篇文章之后,你们都说运行时更新很棒,但 Lighthouse 评分无法接受。因此,我修复了 Lighthouse 评分。下面我将解释。我还被要求补充背景,关于所使用的 Web 应用程序分类,和 SSR 以及 hydration 之类的问题。
因此,让我们从这两种非常常见但又不同类型的应用程序的角度来思考。
- Web 站点和在线电商
优化初始加载体验:清除缓存,在变得可交互之前获取首个可见内容 - 企业应用和网络社交
优化快速页面导航、运行时性能,包括快速页面重新加载。
我开发 Neo 以构建复杂的的 Web 应用程序(RIA),具备强大/胖浏览器客户端 Web 应用————主要是针对的是第 2类的的应用(企业应用和网络社交)进行优化。
请注意,交互式仪表盘类型应用实际上是高性能(RIA)/胖浏览器客户端Web应用的典型代表。
关于快速更新与提高 Lighthouse 评分,Neo 采取的方法是:
- 初始页面加载时完全不包含任何 html 内容————从一个空 body 标签开始,并且没有直接包含样式表。
- 立即在客户端加载小型的 javascript html 生成器————浏览器可以缓存这些,这样就不需要重新获取。
- 发送 JSON 配置数据和 JSON 或其他内容数据,生成器随后使用这些数据填充和更新 DOM ……即该 body 标签的内容。
<!DOCTYPE HTML>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta charset="UTF-8">
<title>My App</title>
</head>
<body>
<script src="../../src/MicroLoader.mjs" type="module"></script>
</body>
</html>
Neo 客户端————组装好的 JavaScript 生成器————拥有独立构建复杂、动态用户界面的所有工具。无需从服务器流式传输 HTML(即SSR),也无需处理水合的复杂性。
提示:这种方法可以节省大量流量,也就是节省资金。
Neo.mjs 落地页
Neo.mjs Learning 部分
2.Neo应用程序有什么特别之处?
我在上一篇博文中展示了Neo应用程序的开发模式。该版本包含最新的 ECMAScript 代码,可直接在浏览器中运行。无需编译或转译。显然,这些代码没有经过压缩。
当我们要部署应用程序时,我们希望发布的是 dist/production
版本,该版本会通过 Webpack、ESBuild 或 Vite 等工具进行压缩和bundle。
问题是:如果我们想让应用程序用户在运行时生成自己的代码,那么应用程序 JavaScript 代码库的部分内容就无法bundle。
在 Neo.mjs 学习部分和落地页中,我们要使用 Monaco 编辑器(在 VSCode 中也会用到)。这只"怪兽"的文件大小超过 3MB,因此要在 Lighthouse 上取得好成绩并不容易。
code.LivePreview: Source View
在这个代码编辑器中,我们可以import
任何 Neo.mjs 类(模块)。
code.LivePreview: Preview View
生成的 DOM 码不是放入iframe中,而是直接安装到 DOM 中。因此,import 的 JavaScript 模块与主应用程序位于同一域。
提示:我说的不是主线程,而是应用程序 SharedWorker
code.LivePreview: DOM 输出
这种情况虽然特殊,但打包工具根本无法处理。除非我们想为所有可能的模块组合创建分块。这样做的开销将是巨大的。
所以,简而言之:我们希望主应用程序运行在经过压缩的 dist/production
模式中,而 code.LivePreview 的所有生成模块都需要运行在未编译的 development
模式中。我们希望这两种版本在同一个 Worker 线程中运行并能够直接通信。我们将很快深入探讨这一点。
3.高交互的多窗口视图
我们可以轻松地将登陆页面上的演示分离到多个浏览器窗口中。它们不仅可以共享状态和数据,而且连接应用的所有组件都位于同一个应用 Worker 中,并可以直接通信。
在一个窗口内的Helix演示
Helix 演示控制扩展到第二个窗口
1 个窗口中的 SocketConnection 控制面板
从 LivePreview 分离出来的 SocketConnection 控制面板
带有 2 个分离窗口的 SocketConnection 仪表板
值得一提的是,我们甚至没有创建新的 Widget 实例。我们只需在不同的浏览器窗口中移动相同 JavaScript 实例的 DOM 输出即可。这还会触发跨窗口 delta CSS 更新。
4.在线演示链接
先快速了解一些信息:
- 最好在桌面浏览器中打开应用程序,以便充分享受多窗口演示。
- 确保启用页面弹出窗口(承诺无广告。)
- 使用 Chrome时,多窗口功能也可在安卓设备上运行。
- 在没有多窗口功能的情况下,该应用程序可在 iOS 上运行
dist/production
生产版本(这是您要测试性能的版本):
https://neomjs.com/dist/production/apps/portal/#/home
development
版本(以防万一您想深入了解非压缩代码):
https://neomjs.com/apps/portal/#/home
提示:如果调试 SharedWorkers,您需要打开 chrome://inspect/#workers。
尤其是在学习部分内部导航,现在有了令人惊叹的性能。
你可以在这里找到该应用程序的源代码(完全 MIT 许可):
neo/apps/portal at dev · neomjs/neo
我们可能会在未来的版本中禁用立方体布局(切换到卡片布局)。只是把它作为一个复活节彩蛋,可以在运行时启用。有什么想法?
5.如何在一个页面上同时运行一个应用程序的开发版本和发行/生产版本?
在此之前,Neo.mjs 6.x 版本根本无法做到这一点。
当同一个框架的多个版本或环境在同一个页面上运行时,可能会发生很糟糕的事情。想想多个版本的 IdGenerator
会导致 DOM id 不再唯一。
还考虑使用指向不同文件的 SharedWorkers → 完全不再“共享”。
让我们来看一个非常简单的 Neo.mjs 类,以发现这一变化:
import Component from './Base.mjs';
class Label extends Component {
static config = {
className: 'Neo.component.Label'
// ...
}
}
Neo.setupClass(Label)
export default Label;
setupClass()
应用了原型链上的所有静态配置,并且我们确实返回了调整后的标签类。
新版本:
import Component from './Base.mjs';
class Label extends Component {
static config = {
className: 'Neo.component.Label'
// ...
}
}
export default Neo.setupClass(Label);
我们不再导出 Label 类本身,而是导出 setupClass()
的输出。现在,该方法将检查给定的 className 命名空间内是否已有内容,如果有,则返回 "缓存" 版本。
这种变化非常简单,但却非常强大。
为了确保重复使用相同的 SharedWorker 实例,我们还需要做一个改动:当打开一个新的浏览器窗口时,我们需要检查应用程序 Worker 运行的环境。
这意味着:Web应用程序在生产模式下运行,而 LivePreview 中的 Helix 在开发模式下运行。因此,在打开窗口以分离控件时,窗口需要打开 dist/production 模式 Worker 的文件或url,尽管它可以从开发模式调入模块。
6.在测量性能方面,有没有比 Lighthouse 更好的替代方案?
Chrome最近发布了长动画帧 API:
这听起来令人惊叹,因为我们终于获得了一些真正的基准测试数据。
我们计划在多个框架和库中创建相同的演示应用程序,以衡量它们的导航性能。
非常欢迎您在 Slack上与我们联系,如果您想在这个问题上提供帮助的话。
7.我们能否进一步提高该应用程序的性能?
我们可以,也愿意。
- assets 尚未 bundle(待办)
- 为关键渲染路径预加载资源(也属于待办)
- 我们可以进一步加强跨 Worker 通信
- 为增量更新确定 Tree 的范围(可能是下一个主版本)
如果您有兴趣深入了解一些技术方面的问题,欢迎留言。
快乐编码,
托比亚斯