当前位置: 首页 > news >正文

前端日志回捞系统的性能优化实践|得物技术

一、前言

在现代前端应用中,日志回捞系统是排查线上问题的重要工具。然而,传统的日志系统往往面临着包体积过大、存储无限膨胀、性能影响用户体验等问题。本文将深入分析我们在@dw/log和@dw/log-upload两个库中实施的关键性能优化,以及改造过程中遇到的技术难点和解决方案。

核心优化策略概览:

我们的优化策略主要围绕三个核心问题:

  • 存储膨胀问题 - 通过智能清理策略控制本地存储大小
  • 包体积问题 - 通过异步模块加载实现按需引入
  • 性能影响问题 - 通过队列机制和节流策略提升用户体验

二、核心性能优化

优化一:智能化数据库清理机制

问题背景

传统日志系统的一个重大痛点是本地存储无限膨胀。用户长期使用后,IndexedDB 可能积累数万条日志记录,不仅占用大量存储空间,更拖慢了所有数据库查询和写入操作。

解决方案:双重清理策略

我们实现了一个智能清理机制,它结合了两种策略,并只在浏览器空闲时执行,避免影响正常业务。

  • 双重清理
    • 按时间清理: 删除N天前的所有日志。
    • 按数量清理: 当日志总数超过阈值时,删除最旧的日志,直到数量达标。
/*** 综合清理日志(同时处理过期和数量限制)* @param retentionDays 保留天数* @param maxLogCount 最大日志条数*/
async cleanupLogs(retentionDays?: number, maxLogCount?: number): Promise<void> {if (!this.db) {throw new Error('Database not initialized')}try {// 先清理过期日志if (retentionDays && retentionDays > 0) {await this.clearExpiredLogs(retentionDays)}// 再清理超出数量限制的日志if (maxLogCount && maxLogCount > 0) {await this.clearExcessLogs(maxLogCount)}} catch (error) {// 日志清理失败不应该影响主流程console.warn('日志清理失败:', error)}
}

  • 智能调度
    • 节流: 保证清理操作在短时间内(如5分钟)最多执行一次。
    • 空闲执行: 将清理任务调度到浏览器主线程空闲时执行,确保不与用户交互或页面渲染争抢资源。
/*** 检查并执行清理(节流版本,避免频繁清理)*/
private checkAndCleanup = (() => {let lastCleanup = 0const CLEANUP_INTERVAL = 5 * 60 * 1000 // 5分钟最多清理一次return () => {const now = Date.now()if (now - lastCleanup > CLEANUP_INTERVAL) {lastCleanup = nowexecuteWhenIdle(() => {this.performCleanup()}, 1000)}}
})()

优化二:上传模块的异步加载架构

问题背景

日志上传功能涉及 OSS 上传、文件压缩等重型依赖,如果全部打包到主库中,会显著增加包体积。更重要的是,大部分用户可能永远不会触发日志上传功能。

解决方案:动态模块加载

189KB 的包体积是不可接受的。分析发现,包含文件压缩(JSZip)和OSS上传的 @dw/log-upload模块是体积元凶,但99%的用户在正常浏览时根本用不到它。

我们采取了“核心功能+插件化”的设计思路,将非核心的上传功能彻底分离。

  • 上传模块分离: 将上传逻辑拆分为独立的@dw/log-upload库,并通过CDN托管。
  • 动态加载实现: 仅在用户手动触发“上传日志”时,才通过动态创建script标签的方式,从CDN异步加载上传模块。我们设计了一个单例加载器确保模块只被请求一次。
/*** OSS 上传模块的远程 URL*/
const OSS_UPLOADER_URL = 'https://cdn-jumper.dewu.com/sdk-linker/dw-log-upload.js'/*** 动态加载远程模块* 使用单例模式确保模块只加载一次*/
const loadRemoteModule = async (): Promise<LogUploadModule> => {if (!moduleLoadPromise) {moduleLoadPromise = (async () => {try {await loadScript(OSS_UPLOADER_URL)return window.DWLogUpload} catch (error) {moduleLoadPromise = nullthrow error}})()}return moduleLoadPromise
}/*** 上传文件到 OSS*/
export const uploadToOss = async (file: File, curEnv?: string, appId?: string): Promise<string> => {try {// 懒加载上传函数if (!ossUploader) {const module = await loadRemoteModule()ossUploader = module.uploadToOss}const result = await ossUploader(file, curEnv, appId)return result} catch (error) {console.info('Failed to upload file to OSS:', error)return ''}
}

优化三:JSZip库的动态引入

我们避免将 JSZip 打包到主库中,从主包中移除,改为在上传模块内部动态引入,优先使用业务侧可能已加载的全局window.JSZip。

/*** 获取 JSZip 实例*/
export const getJSZip = async (): Promise<JSZip | null> => {try {if (!JSZipCreator) {const module = await loadRemoteModule()JSZipCreator = module.JSZipCreator}zipInstance = new window.JSZip()return zipInstance} catch (error) {console.info('Failed to create JSZip instance:', error)return null}
}// 在上传模块中实现灵活的 JSZip 加载
export const JSZipCreator = async () => {// 优先使用全局 JSZip(如果页面已经加载了)if (window.JSZip) {return window.JSZip}return JSZip
}

优化四:日志队列与性能优化

在某些异常场景下,日志会短时间内高频触发(如循环错误),密集的IndexedDB.put()操作会阻塞主线程,导致页面卡顿。

我们引入了一个日志队列,将所有日志写入请求“缓冲”起来,再由队列控制器进行优化处理。

  • 限流: 设置每秒最多处理的日志条数(如50条),超出部分直接丢弃。错误(Error)级别的日志拥有最高优先级,不受此限制,确保关键信息不丢失。
  • 批处理与空闲执行: 将队列中的日志打包成批次,利用requestIdleCallback在浏览器空闲时一次性写入数据库,极大减少了 I/O 次数和对主线程的占用。
export class LogQueue {private readonly MAX_LOGS_PER_SECOND = 50/*** 检查限流逻辑*/private checkRateLimit(entry: LogEntry): boolean {// 错误日志总是被接受if (entry.level === 'error') {return true}const now = Date.now()if (now - this.lastResetTime > 1000) {this.logCount = 0this.lastResetTime = now}if (this.logCount >= this.MAX_LOGS_PER_SECOND) {return false}this.logCount++return true}
}

空闲时间处理机制:

export function executeWhenIdle(callback: () => void, timeout: number = 2000): void {if (typeof window !== 'undefined' && 'requestIdleCallback' in window) {window.requestIdleCallback(() => {callback()}, { timeout })} else {setTimeout(callback, 50)}
}

三、打包构建中的技术难点与解决方案

在改造过程中,我们遇到了许多与打包构建相关的技术难题。这些问题往往隐藏较深,但一旦出现就会阻塞整个开发流程。以下是我们遇到的主要问题和解决方案:

难点一:异步加载 import()

打包失败问题

问题描述

await import('./module')语法在 Rollup 打包为 UMD 格式时会直接报错,因为 UMD 规范本身不支持代码分割。

// 这样的代码会导致 UMD 打包失败
const loadModule = async () => {const module = await import('./upload-module')return module
}

错误信息:

Error: Dynamic imports are not supported in UMD builds
[!] (plugin commonjs) RollupError: "import" is not exported by "empty.js"

解决方案:inlineDynamicImports 配置

通过在 Rollup 配置中设置inlineDynamicImports: true来解决这个问题:

// rollup.config.js
export default {input: 'src/index.ts',output: [{file: 'dist/umd/dw-log.js',format: 'umd',name: 'DwLog',// 关键配置:内联动态导入inlineDynamicImports: true,},{file: 'dist/cjs/index.js',format: 'cjs',// CJS 格式也需要这个配置inlineDynamicImports: true,}],plugins: [typescript(),resolve({ browser: true }),commonjs(),]
}

配置说明

  • inlineDynamicImports: true会将所有动态导入的模块内联到主包中
  • 这解决了 UMD 格式不支持动态导入的问题

难点二:process对象未定义问题

问题描述

打包后的代码在浏览器环境中运行时出现process is not defined错误:

ReferenceError: process is not definedat Object.<anonymous> (dw-log.umd.js:1234:56)

这通常是因为某些 Node.js 模块或工具库在代码中引用了process对象,而浏览器环境中并不存在。

解决方案:插件注入 process 对象

我们使用@rollup/plugin-inject插件,在打包时向代码中注入一个模拟的process 对象,以满足这些库的运行时需求。

  • 创建process-shim.js文件提供浏览器端的process实现。
  • 在rollup.config.js中配置插件:
// rollup.config.js
import inject from '@rollup/plugin-inject'
import path from 'path'export default {// ... 其他配置plugins: [// 注入 process 对象inject({// 使用文件导入方式注入 process 对象process: path.join(__dirname, 'process-shim.js'),}),typescript(),resolve({ browser: true }),commonjs(),]
}

创建 process-shim.js 文件:

// process-shim.js
// 为浏览器环境提供 process 对象的基本实现
export default {env: {NODE_ENV: 'production'},browser: true,version: '',versions: {},platform: 'browser',argv: [],cwd: function() { return '/' },nextTick: function(fn) {setTimeout(fn, 0)}
}

高级解决方案:条件注入

为了更精确地控制注入,我们还可以使用条件注入:

inject({// 只在需要的地方注入 processprocess: {id: path.join(__dirname, 'process-shim.js'),// 可以添加条件,只在特定模块中注入include: ['**/node_modules/**', '**/src/utils/**']},// 同时处理 global 对象global: 'globalThis',// 处理 Buffer 对象Buffer: ['buffer', 'Buffer'],
})

难点三:第三方依赖的

ESM/CJS兼容性问题

问题描述

某些第三方库(如 JSZip、@poizon/upload)在不同模块系统下的导入方式不同,导致打包后出现导入错误:

TypeError: Cannot read property 'default' of undefined

解决方案:混合导入处理

// 处理 JSZip 的兼容性导入
let JSZipModule: any
try {// 尝试 ESM 导入JSZipModule = await import('jszip')// 检查是否有 default 导出JSZipModule = JSZipModule.default || JSZipModule
} catch {// 降级到全局变量JSZipModule = (window as any).JSZip || require('jszip')
}// 处理 @poizon/upload 的导入
import PoizonUploadClass from '@poizon/upload'// 兼容不同的导出格式
const PoizonUpload = PoizonUploadClass.default || PoizonUploadClass

在 Rollup 配置中加强兼容性处理:

export default {plugins: [resolve({browser: true,preferBuiltins: false,// 解决模块导入问题exportConditions: ['browser', 'import', 'module', 'default']}),commonjs({// 处理混合模块dynamicRequireTargets: ['node_modules/jszip/**/*.js','node_modules/@poizon/upload/**/*.js'],// 转换默认导出defaultIsModuleExports: 'auto'}),]
}

四、性能测试与效果对比

打包优化效果对比:

五、总结

通过解决这些打包构建中的技术难点,我们不仅成功完成了日志系统的性能优化,还积累了工程化经验。这些实践不仅带来了日志系统本身的轻量化与高效化,其经验对于任何追求高性能和稳定性的前端项目都有部分参考价值。

往期回顾

1. 得物灵犀搜索推荐词分发平台演进3.0

2. R8疑难杂症分析实战:外联优化设计缺陷引起的崩溃|得物技术

3. 可扩展系统设计的黄金法则与Go语言实践|得物技术

4. 营销会场预览直通车实践|得物技术

5. 基于TinyMce富文本编辑器的客服自研知识库的技术探索和实践|得物技术

文 / 沸腾

关注得物技术,每周更新技术干货

要是觉得文章对你有帮助的话,欢迎评论转发点赞~

未经得物技术许可严禁转载,否则依法追究法律责任。


文章转载自:

http://296sR8na.skmzm.cn
http://Z9Unymm3.skmzm.cn
http://xIsElj6O.skmzm.cn
http://5xHMHaPz.skmzm.cn
http://4nGp2iG0.skmzm.cn
http://pDYz91JX.skmzm.cn
http://Txbx64XB.skmzm.cn
http://w2QcN51D.skmzm.cn
http://t3CFME17.skmzm.cn
http://iprzXCyN.skmzm.cn
http://VMEFFYQS.skmzm.cn
http://w9R70wPF.skmzm.cn
http://547q0yHj.skmzm.cn
http://uAwHQvup.skmzm.cn
http://H1CVHANK.skmzm.cn
http://Qz4kcwF2.skmzm.cn
http://C14wZggU.skmzm.cn
http://YMVgpRfv.skmzm.cn
http://fik5pOSE.skmzm.cn
http://dfqErgPK.skmzm.cn
http://gliFKhoQ.skmzm.cn
http://ftMPP9a5.skmzm.cn
http://Cvq08fm4.skmzm.cn
http://XJJtlVf9.skmzm.cn
http://oEmKLa6E.skmzm.cn
http://sGjGHZS7.skmzm.cn
http://jWGLG8Hl.skmzm.cn
http://xrgLtZxu.skmzm.cn
http://jdsgdF16.skmzm.cn
http://jSm2LmSs.skmzm.cn
http://www.dtcms.com/a/375696.html

相关文章:

  • 基于R语言机器学习方法在生态经济学领域中的实践技术应用
  • 【1分钟速通】 HTML快速入门
  • Spring IocDI(二)
  • 《QT 108好类》之16 QComboBox类
  • 物联网平台中的MongoDB(一)服务模块设计与架构实现
  • QT里的QSlider滑块样式设计【记录】
  • HTTP/3.0:网络通信的技术革新与性能飞跃
  • Spring Boot--yml配置信息书写和获取
  • 笔记7 FreeRTOS低功耗模式和内存管理
  • 慧荣SM770新一代USB显示接口芯片方案,支持三路并行4K显示扩展方案
  • 嵌入式基础知识——关键字
  • 小红书卡片制作源码后台
  • MySQL,SQL Server,PostgreSQL三种数据库的区别
  • 基于Yolov8实现在Label-Studio实现半自动标注
  • Spring Boot---自动配置原理和自定义Starter
  • NFS资源共享服务
  • 新手向:Python网络编程,搭建简易HTTP服务器
  • RNN循环神经网络(一):基础RNN结构、双向RNN
  • 牛刀小试之设计模式
  • openCV3.0 C++ 学习笔记补充(自用 代码+注释)---持续更新 四(91-)
  • leetcode-python-1941检查是否所有字符出现次数相同
  • python内存分析memory_profiler简单应用
  • 9.9 json-server
  • excel中筛选条件,数字筛选和文本筛选相互转换
  • zsh: no matches found: /Users/xxx/.ssh/id_rsa*
  • 【EPGF 白皮书】路径治理驱动的多版本 Python 架构—— Windows 环境治理与 AI 教学开发体系
  • C语言面向对象编程:模拟实现封装、继承、多态
  • 设计 模式
  • 【Scientific Data 】紫茎泽兰的染色体水平基因组组装
  • MVCC-多版本并发控制