记 LobeHub 的性能和 DX 优化
该渲染由 Shiro API 生成,可能存在排版问题,最佳体验请前往:https://innei.in/posts/tech/lobehub-performance-dx-optimization 距离去新的工作差不多一个多月了。这段时间刚入职,其实都挺忙的,而且公司也正在筹划发布 2.0 的版本,所以我加入的时候刚好算是个好时机(忙到起飞)。 这一个月基本都是在冲刺。我发现,我每次换一个公司,刚进去的一段时间都会去做一些项目中的性能优化,以及对一些开发体验不友好的事情,做过多的调教。 尤其是现在大语言模型在编程领域的能力过于出色,到了一个人人都能写代码的时代。 在代码由 AI 快速生成的环境下,很难做到对代码质量的可控。尤其是对于 React 来说,大家往往会为了追求性能最优,而忽视了代码质量这一点。我也算是 LobeHub 的早期用户之一。在很长一段时间里,大家对它的印象其实就是很卡,特别卡。 我也在自己的服务器上部署过,可能本地部署的感知程度会稍微小一点,但不管怎么样,性能问题一直都是大家口中的痛点。 刚好在这里,我先来总结一下过去一个月我对 LobeHub 做的一些性能优化,以及它到底提升了多少性能。 Infra/Performance react-layout-kit 替换 这个库是一个 CSS-in-JS 的 Flexbox 组件库。现在有了 Tailwind 的话,编写 Flexbox 布局也是能非常快写出来的。但由于 LobeHub 没有使用 Tailwind,所以就有了这样一个库,能够更快地编写 Flexbox 并设定其各种属性。由于是 Flexbox,所以在项目中大量使用。一个页面可能有几百个这样的组件,所以数量是非常庞大的。 通过火焰图的对比,其实单个组件的性能开销也还好,基本上在 0.1ms 左右。当然,如果是一个普通的 div 组件,这种开销通常小于 0.1ms。 但在组件使用非常多的情况下,0.1ms 的量级累积起来其实也会很大。当然,从性能上来说,其实感知的开销还好。但另外一个问题是:CSS-in-JS 这种方式,只要每个 Flexbox 的属性没有相同的地方,就会生成大量的 CSS。 由于它是 CSS-in-JS 的实现,生成的每一段 CSS 都会比较占用内存。特别是开发环境下。 下面那个测试链接也是我当时写的,主要是对比两种方案的性能、渲染时长和内存占用: 纯 CSS 的实现 基于 Emotion 的 CSS-in-JS 方案 https://css-in-js-batch.vercel.app/ 可以看到,虽然两者的开销差别并不是很大,但在内存占用方面,差距达到了大概十个量级左右。 当然,在生产环境下可能没有这么夸张,但依然会有明显的性能提升。 https://github.com/lobehub/lobe-ui/pull/424/changes#diff-42328900eb42223334f9d29281070d175ac52eb619023cc07effc02fc62be8dfR6 CSS in JS hook 接下来这个问题,还是通过火焰图排查渲染的情况发现的。 在业务中,由于大量使用了动态生成的 CSS-in-JS 的 React Hook,产生了这个 Hook 的性能开销比较大的问题: 每次 Re-render 都会重新跑一遍 只要 Re-render 的数量特别大,性能上就会感到非常卡顿 另外,Hook 几乎被所有组件所使用。 所以,如果一个页面上有几百个组件同时都在运行这个 Hook,那么这次 layout 基本上一定会花费大量时间来处理。 这个问题的发现,也是通过火焰图排查到有一些比较轻的组件,渲染却用了一毫秒。在开发环境下要花一毫秒左右的时间,就感到特别奇怪。 然后去排查是哪个 Hook 导致的,最后发现是一个 useStyle 的钩子。 找到问题之后,就比较好解决了,我们只要换一种方案就行。 在这里,我们提出了一种优化方案:不再使用动态生成 CSS-in-JS 的方案,而是使用静态方案。这样的话,性能上也会提升不少,毕竟在组件的运行时上不会再有额外的开销了。 https://github.com/lobehub/lobe-ui/pull/437 https://github.com/ant-design/antd-style/pull/205 首屏离屏优化 上面这两个是比较重大的优化。这波优化全部完成之后,整个 APP 已经有了很明显的提升。 以前消息列表中的每个 Message Item 都需要花很长时间去渲染,而现在在开发环境的表现已经和之前在生产环境差不多了,在生产环境上则会更快。内存表现也是:现在可以保持在正常情况下 400-500 MB 的样子,甚至更低。 首屏的性能优化,主要做的是如何让用户从其他页面返回首屏时速度更快。 首屏的加载时间是非常影响用户体验的一个点。在 React 中,当一个页面切换到另一个页面时,上一个页面一般都会被销毁。如果首屏组件的逻辑很重,从另一个页面返回首页时,就需要等待首页的重建。 用户需要等一段时间,而这段时间页面相当于处于不可用的状态。 可以看到上面那张优化前的图。从其他路由返回到首页时,Desktop Home Layout 这个组件在重新渲染时大概花了 500ms。 使用 React 的 Offscreen(也就是 Activity 组件)优化之后,从其他页面返回到首屏的时间控制在 55ms 左右,几乎在点击的瞬间就可以完成跳转。 这是在开发环境下的火焰图表现,在生产环境下,它的速度会更快。 https://github.com/lobehub/lobe-chat/pull/10890 基础组件的优化 完成了上述内容后,在消息列表中,单个 MessageItem 的首次渲染性能有了非常大的进步。 但是,MessageItem 组件过于复杂,部分基础组件如果渲染开销较大,也会影响整个 MessageList 的性能。 首先重构了 AccordionItem 组件,这类组件在未展开时,内容区的 React 组件逻辑通常不必执行。因为它在 UI 中本身不可见,那么对应的组件逻辑也不应执行。 这是一种常见的优化手段,在比较复杂的组件中,能够显著提升性能。 https://github.com/lobehub/lobe-ui/pull/430 Tabs 也是基础的 UI 组件。在之前的 AntD 实现中,单个 Tooltip 的渲染性能开销非常大,在开发环境中平均需要 0.5ms。 在过去的多个PR中,我使用了 Base UI 以及组件单例的方案,重构了包括 Tooltip 和 Popover 两个组件。 目前在业务中,基本已经把 Tooltip 替换完毕了,Popover 的话还有剩余一些。更换之后,这两个组件的性能比之前有了很大的提升。 https://github.com/lobehub/lobe-ui/pull/448 还有其他一些基础组件也还有优化的空间,但目前还没有细看。 后续的计划如下: 稳步迁移更多组件 将组件从 AntD 迁移到 Base UI 或者其他 Headless 库 剪裁 Desktop App 体积 关于裁剪 Electron 体积的话,其实我之前也分享过经验。 首先在 Mac 上,Electron Framework 的体积是可以裁剪的。它里面自带了很多语言包,占用大约 34MB 左右。我们可以把它们全部删掉,只留一个英语(en.lproj)就可以了。需要注意的是: 如果一个都不留,程序会闪退,所以必须保留一个。 在 App 中,这些语言界面我们基本上会自己做一套 i18n 去维护,所以没必要使用它自带的。 通过这种方式,直接就能省掉 ~30MB 左右的体积。 其次,我们可以优化 ElectronBuilder 打包 node_modules 的逻辑。 一般来说,如果 Electron 主进程(Main Process)的打包没有 Native Binding 的话,我们可以利用打包器把所有的三方依赖打进我们的 Bundle 里面。这样一来,程序就可以完全脱离 node_modules 去运行。 然后可以修改 ElectronBuilder 的配置在配置中将 node_modules 排除掉。 当然,这里考虑到后面可能会使用到 Native Binding,所以我在里面做了一层兼容性。 如果有 Native Binding 的话,我们可以把单独的 Native Binding 依赖加进配置,这样一来,这个依赖就会被打到 asar.unpack 里面。 详细的配置我就贴在下面,大家有兴趣可以看一看: https://github.com/lobehub/lobe-chat/blob/9145bc36b0af7da74741e3c5fd8ed61744e0a3d1/apps/desktop/electron-builder.mjs https://github.com/lobehub/lobe-chat/pull/11397 经过这些处理,大概裁剪了 100MB 左右。现在 App 的体积大概在 260MB 左右。 DX 上面就是一些性能优化的点。 接下来,我想说一说影响开发体验(DX)的一些优化吧。 首先是我很早以前跟空谷提过的建议:把 i18n 的 key 扁平化,不要使用对象嵌套的方式。 这样做的原因主要有以下几点: 定位方便:你可以直接通过这个 key 在代码中找到对应的文案位置。 操作简单:如果是对象嵌套的话,你不能直接复制 key 过来(因为 key 是一层一层的,你必须手动去拼接),而扁平化的 key 则可以直接复制使用。 也是终于把这个事情给做掉了。 还有一个也是我想想安利一下的,就是我之前写过的一个库:electron-ipc-decorator。我也把这个库给实装了。 因为之前的实现需要一个 dispatch,然后再去做 subscribe,类型化也不太安全,而且 dispatch 的 type 也都是 hardcode 的一个 string 编码,我感觉不是很好。 所以我把自己这一套也换了上去,现在写起来比较方便。 https://github.com/lobehub/lobe-chat/pull/10679 剩下的话其实还是在做一些计划。 比较大的一个重构点,应该就是把 Next.js 迁移到 Vite。这个计划的工程量很大,目前还在筹划中。我也做了一些实验,应该还是有可行性的。 如果这个能成功的话,那应该也是很多开发者的一个福音了。 不管是团队内的人还是社区的人,现在应该都能很明确地感受到,现在 Next.js Server 启动的话,大概要十多个 G 的运行内存。如果你是 16G 的机器,基本上是没有办法开发的。 而通过实验做下来的结果是:Vite 可以控制开发时的运行内存,只需要大概 1G 多一点就行了。相比之下,内存占用和现在相比大概减少了 10 倍左右。 https://github.com/lobehub/lobe-chat/discussions/10830 看完了?说点什么呢