桌面端 → 深入分析与方案实践

Electron在新版QQ中的实践与优化

技术背景

老QQ存在的技术债
  • 多端代码不统一

初版的QQ采用纯原生的方式进行开发,大小在200K左右,在后期的智能设备的普及下,开始像移动端、桌面端做转型,针对三大操作系统陆续组建了三个不同的开发团队,各自负责对应的技术代码;

  • 多端功能不一致

旧版的额桌面端QQ,Windows 的功能最丰富,Mac OS 次之, Linux 功能非常简洁;这样会不利于用户对于QQ的统一认知,因此功能的统一迫在敏捷

初版的重构 → QQ NT项目

QQ NT项目是在 2022 年 3 月份正式启动, Mac OS QQ 在 6 月份开始发布内测, 9 月份正式上架了 App Store,迭代了几个版本之后,QQ 团队就同步开发 Linux。

在 2022 年,QQ 发布了新的 macOS 和 Linux 版本,包括 QQ 后台其实也做了很大的改变和重构,核心系统做了全新重写,云原生成熟度也得到了很大的提升。
在目前全新的框架设计下,无论是核心系统、功能迭代还是设计语言上,都可以尽可能地"原子化",来让 QQ 后续更好地迭代功能。

重构路上的挑战

业务功能上的挑战

历史原因,项目自身有很多的历史功能,如何取舍、如何重构、如何拓展等就具有相当的挑战,小小的改动有可能就会带来很激进的用户反馈

技术重构上的挑战

多端技术栈场景下的功能尽可能统一的挑战也相当大,尤其是Linux上

技术选型到Electron的原因

相比于QT、RN、Flutter、Webview2等,会有技术栈深入的问题考量,同时还需要考虑发展问题;有些框架技术,虽然它很热,但我们历史上踩过了很多很多非标准化的坑,一旦某个技术栈热度一过、维护力度不够,它就会成为全新的负债,做选型时必然也是避免再有类似经历。

选择Electron主要基于以下几点考量
  • 首先最看重的是框架成熟度和技术栈的标准化

Electron是基于Web技术的,上手成本很低,且对于现有的代码有较高的复用率,不太需要太多的成本去做基建和周边工具的建设,之前的RN、Flutter的实践上就有类似的情况,同时由于其对于Web技术的支持,使得后续的版本迭代和重构效率有很大的提升;

  • 其次是技术经验及人才储备

Qt 的确在性能上是一个很好的选择,但目前团队对 Qt 没有太多积累,基建基本没有,而且相关人才其实比较匮乏,招聘就更难了。 而当前 QQ 技术团队 Web 前端团队还是有比较多的积累,在 QQ 频道项目中,也完整验证了 Electron 的技术可行性。

  • 最后就是 Electron 具备的桌面端跨平台的优势

QQ NT 架构并不是仅指 Electron,Electron 主要是作为 UI 跨平台的框架,只是占比很小的一部分,并且 QQ 桌面端不是全部用 Electron 实现,QQ NT 最核心的部分还是 QQ 底层通用抽象的模块,称之为 NT 内核,包括核心登录、消息系统、关系链、富媒体、长连接、数据库等等模块,完全用 C++ 实现,全平台通用。因此底层是完全跨平台的架构,而 Electron 只是上层桌面端 UI 跨平台较薄的一层。

在内存方面的优化实践

可以在于一系列的针对性的优化策略,包括缓存策略、按需加载、优雅降级等,同时配合各种分析工具、线上监控、自动化测试手段等来组织性能的退化

新版QQ在内存上的挑战
  • 产品形态

在常规情况下,由一个复杂的大面板(100+复杂程度不等的模块)和一系列独立功能窗口构成。窗口和渲染进程一一对应,窗口进程数很大程度上影响了Electron的内存占用;

  • 使用习惯

用户长时间挂机。相比用完即走的 Web 页面,QQ 用户在一次登录后,可能会挂机一个月以上。这段期间,如果没有控制好 QQ 内存使用,那么结果可能是内存越占越大、用户交互响应变慢、甚至发生闪退。

  • 版本迭代

需要快速的补齐不同版本间的核心功能,同时一些较高优先级的新功能也需要同步产出,快速的新旧需求的迭代有可能也会引起新的问题导致性能劣化;

  • 应用框架

新版 QQ 依赖一个 NT 核心数据模块(C++ addon),为 UI 提供本地化的数据服务。QQ 的加载体验能做到如此丝滑,这个模块起到了至关重要的作用。 同时,与 NT 的联动优化,也需要拉通客户端 C++ 开发同学共同完成。当然,会存在一些沟通成本,但不可否认,能把内存占用压下来,客户端同学也付出了非常多的努力。

新版QQ的内存优化目标

AIO是指聊天面板的简称

  • 第一阶段:单进程内存<300M
  • 第二阶段:单进程 < 100M,整体 < 300M
  • 需要额外重点关注的点
    • node:Electron 的主进程,负责窗口管理、跨进程通信等。包含 NT 核心数据模块,负责与服务端交互,为 UI 提供数据服务;
    • renderer:Chromium 内核的渲染进程,负责渲染 UI、提供用户交互等。QQ 启动后,会有 2 个渲染进程:一个是 QQ 大面板,另一个是主进程的窗口池。窗口池是预创建的一个渲染进程。在新开窗口时,可以减少等待时间;
    • gpu:Chromium 内核的 GPU 进程。它的主要作用是处理与图形相关的任务,例如渲染网页、播放视频、执行动画等。

具体的优化方案

分析工具侧(不仅仅包含V8的JS部分,还有很多NT 核心数据模块(C++ addon)的C++模块)

需要使用不同维度的内存分析工具,从 V8 引擎到进程,再到整个应用程序,打通整个链路进行多角度的细节分析,以此来定位内存使用的瓶颈。

定向优化

在通过工具定位到问题之后,我们会采取一系列的针对性优化策略,包括缓存策略、按需加载、优雅降级等。

同时也需要进行项目整体的代码压缩瘦身、静态资源优化、分包和按需加载等优化措施配合实现;

  • 第三方包或者SDK,一般会包含完整的能力,但是在某些场景下是不需要全量引入的,因此在这些资源没有提供按需引入和Tree-Shaking等技术前提下,就需要进行定制化裁剪或者单独实现了,从而减少项目依赖资源的代码体积;
  • 项目自身方面需要配合分包策略来实现,而分包策略不会完全按照某个规定的方面进行定制,而是按照场景模块来进行细粒度的定制;
  • 其他静态资源可以使用按需加载的策略,可见时加载,不可见时主动回收和销毁
窗口池方案浅析
  • 窗口池中预启动的窗口页面只加载必须执行的基础代码;
  • 当打开具体窗口时加载对应的路由后页面入口代码;
  • 当具体使用不同功能时动态加载(如点击搜索、打开表情面板、转发消息激活好友选择器的时候才会分别加载对应功能模块代码)
线上监控

在本地进行验证通过后,需要进行线上大范围的实际用户场景验证,从而根据实际的用户反馈来判断是否进行全量发布;

可以通过Electron提供的app.getMeticsprocess.getMemoryInfo等api来采集内存指标,但是需要注意对应的api采集的数据的准确度,有些不一定是你需要的数据

防止性能退化和自动化测试

除了比较常规的单元测试、代码检查、代码评审机制、框架内置一些开发规范等手段外,还建设一个防劣化平台,主要通过自动化的端对端 (e2e) 测试来持续监控项目集成后的性能变化。

  • 定时对主干上集成构建的程序进自动化 e2e 测试;
  • 除了对功能的冒烟测试外,针对重点关注的性能指标,构造了对应的帐号和环境,编辑特定的用例,用于采集性能指标;
  • 通过将采集和采样的指标上报到防劣化的监控平台,来监控项目集成后的性能变化,如会话切换响应时间、内存占用、CPU 使用率等;
  • 监控平台提供按版本和时间的指标曲线、对比,方便查看和分析性能变化情况。同时打通企业微信机器人,对性能指标情况进行实时推送告警。

Electron在vivo中的实践总结

技术背景

因业务发展,需要用到桌面端技术、技术特性涉及离线可用、调用桌面系统能力等要求;

实践细节

常用开发技术栈选型
  • 构建工具选型

主要有electron-builderelectron-forge,最终采用了Electron-Forge进行实现,原因就是其简单又强大

  • monorepo方案选型

主要原则就是集大成法,即取各家之长,如pnpm擅长依赖管理、turbo擅长构建任务编排

  • 本地数据库选型

Electron应用数据库有非常多的选择如 lowdbsqlite3 、 electron-store 、 pouchdb 、 dedb 、 rxdb 、 dexie 、 ImmortalDB 等。这些数据库都有一个特性,那就是无服务器;其部分细节对比如下:

  • lowdb:生态、能力、性能三方面表现优秀, json 形式的存储结构, 支持 lodash 、 ramda 等 api 操作,利于备份和调用;
  • sqlite3:生态、能力、性能三方面表现优秀, Nodejs 关系型数据库第一选择方案;
  • nedb:生态、能力、性能三方面表现优秀,缺点是基本不维护了,但底子还在,尤其操作是 MongoDB 的子集,对于熟悉 MongoDB 的使用者来说是绝佳选择;
  • electron-store:生态表现优秀,轻量级持久化方案,简单易用。
  • pouchdb ,如果需要将本地数据同步到远端数据库,可以使用 pouchdb ,其和 couchdb 可以轻松完成同步
二进制文件(如ffmpeg)构建浅析

在开发桌面端程序时,难免会遇到需要引用第三方的二进制程序,如ffmpeg等,但需要注意以下两点:

  • 二进制程序不能打包进asar中,可以在构建配置文件中进行配置
js 复制代码
// forge.config.js
const os = require('os')
const platform = os.platform()
const config = {
  packagerConfig: {
    // 可以将 ffmpeg 目录打包到 asar 目录外面
    extraResource: [`./src/main/ffmpeg/`]
  }
}
  • 开发和生产环境,获取二进制程序的路径方法是不一样的,可以通过如下方法进行实现
js 复制代码
import { app } from 'electron'
import os from 'os'
import path from 'path'
const platform = os.platform()
const dir = app.getAppPath()
let basePath = ''
if(app.isPackaged) basePath = path.join(process.resourcesPath)
else basePath = path.join(dir, 'ffmpeg')
const isWin = platform === 'win32'
// ffmpeg 二进制程序路径
const ffmpegPath = path.join(basePath, `${platform}`, `ffmpeg${isWin ? '.exe' : '.org'}`)
  • 按需构建,防止将不必要的终端构建结构也打到构建文件中
js 复制代码
// forge.config.js
const os = require('os')
const platform = os.platform()
const config = {
  packagerConfig: {
    extraResource: [`./src/main/ffmpeg/${platform}`]
  },
}
性能优化
  • 构建体积的优化

asar中的文件就是构建后的项目代码,electron 构建机制中,会自动把 dependencies 的依赖全部打到 asar 中。

  • 相关原则
    • 将 web 端构建所需的依赖全部放到 devDependencies 中,只将在 electron 端需要的依赖放到 dependencies

    • 将和生产无关的代码和文件从构建中剔除;

    • 对跨平台使用的二进制文件,如 ffmpeg 进行按需构建(上文按需构建已介绍)

    • 对 node_modules 进行清理精简

      • 对于项目是通过yarn安装的依赖可以通过如下命令实现
      js 复制代码
      yarn autoclean -I
      
      yarn autoclean -F
版本更新
  • 全量更新

    全量更新就是通过下载最新的包或者 zip 文件,进行软件更新,需要替换所有的文件。

    架构流程图

    • 开发服务端接口,获取最新的版本信息
    • 渲染进程通过接口获取最新版本信息,封装更新逻辑,通过返回的版本信息判断是否通过IPC通信通知主进程实现更新
    • 主进程检测渲染进程的消息,通过Electron-updater进行全量更新
    • 将更新信息通过IPC推送给渲染进程
    • 渲染进程向用户展示更新信息,若更新成功,则弹出弹窗告知用户重启应用,完成软件更新
  • 增量更新

    增量更新是通过拉取最新的渲染层打包文件,覆盖之前的渲染层代码,完成软件更新,此方案只需替换渲染层代码,无��替换所有文件。

    • 开发服务端接口,获取最新的版本信息
    • 渲染进程通过接口获取最新版本信息,封装更新逻辑,通过返回的版本信息判断是否通过IPC通信通知主进程实现更新
    • 主进程检测渲染进程的消息,拉取线上最新包
    js 复制代码
    import { app, autoUpdater } from 'electron';
    import isDev from 'electron-is-dev';
    
    if (!isDev) {
      const serverUrl = 'https://your-update-server.com'; // 替换为实际的更新服务器地址或 GitHub Releases 地址
      autoUpdater.setFeedURL(`${serverUrl}/update/${process.platform}/${app.getVersion()}`);
      // 启动自动检查更新  也可通过渲染进程接口请求方式获取
      autoUpdater.checkForUpdatesAndNotify();
    }
    • 将更新信息通过IPC推送给渲染进程
    js 复制代码
    autoUpdater.on('update-available', () => {
      console.log('发现新版本');
    });
    
    // 确认下载完成后触发更新逻辑 此处可以进行用户交互拦截逻辑
    autoUpdater.on('update-downloaded', () => {
      autoUpdater.quitAndInstall();
    });
    • 渲染进程向用户展示更新信息,通过用户行为判断是否进行下载和安装对应的更新包
性能优化
  • 启动时优化

    • 使用v8-compile-cache缓存编译代码
    • 优先加载核心功能,非核心功能动态加载
    • 使用多进程、多线程技术
    • 采用asar打包,加快启动速度
    • 增加视觉过度
  • 运行时优化

    • 渲染进程进行Web性能优化
    • 对主进程进行轻量瘦身

    核心方案就是将运行时耗时、计算量大的功能交给新开的 node 进程去执行处理。

    js 复制代码
    const { fork } = require('child_process')
    let { app } = require('electron')
    
    function createProcess(socketName) {
      process = fork(`xxxx/server.js`, [
        '--subprocess',
        app.getVersion(),
        socketName
      ])
    }
    
    const initApp = async () => {
      // 其他初始化代码...
      let socket = await findSocket()
      createProcess(socket)
    }
    
    app.on('ready', initApp)

推荐文献

全面解密新QQ桌面版的Electron内存优化实践
vivo的Electron技术栈选型、全方位实践总结

相关推荐
东华帝君5 分钟前
react 虚拟滚动列表的实现 —— 动态高度
前端
CptW7 分钟前
手撕 Promise 一文搞定
前端·面试
温宇飞7 分钟前
Web 异步编程
前端
腹黑天蝎座8 分钟前
浅谈React19的破坏性更新
前端·react.js
东华帝君8 分钟前
react组件常见的性能优化
前端
第七种黄昏8 分钟前
【前端高频面试题】深入浏览器渲染原理:从输入 URL 到页面绘制的完整流程解析
前端·面试·职场和发展
angelQ8 分钟前
前端fetch手动解析SSE消息体,字符串双引号去除不掉的问题定位
前端·javascript
Huangyi8 分钟前
第一节:Flow的基础知识
android·前端·kotlin
林希_Rachel_傻希希9 分钟前
JavaScript 解构赋值详解,一文通其意。
前端·javascript
Yeats_Liao10 分钟前
Go Web 编程快速入门 02 - 认识 net/http 与 Handler 接口
前端·http·golang