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

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技术栈选型、全方位实践总结

相关推荐
Мартин.几秒前
[Meachines] [Easy] Sea WonderCMS-XSS-RCE+System Monitor 命令注入
前端·xss
昨天;明天。今天。1 小时前
案例-表白墙简单实现
前端·javascript·css
数云界1 小时前
如何在 DAX 中计算多个周期的移动平均线
java·服务器·前端
风清扬_jd1 小时前
Chromium 如何定义一个chrome.settingsPrivate接口给前端调用c++
前端·c++·chrome
安冬的码畜日常1 小时前
【玩转 JS 函数式编程_006】2.2 小试牛刀:用函数式编程(FP)实现事件只触发一次
开发语言·前端·javascript·函数式编程·tdd·fp·jasmine
ChinaDragonDreamer2 小时前
Vite:为什么选 Vite
前端
小御姐@stella2 小时前
Vue 之组件插槽Slot用法(组件间通信一种方式)
前端·javascript·vue.js
GISer_Jing2 小时前
【React】增量传输与渲染
前端·javascript·面试
eHackyd2 小时前
前端知识汇总(持续更新)
前端
万叶学编程5 小时前
Day02-JavaScript-Vue
前端·javascript·vue.js