一、前言
在现代前端应用中,日志回捞系统是排查线上问题的重要工具。然而,传统的日志系统往往面临着包体积过大、存储无限膨胀、性能影响用户体验等问题。本文将深入分析我们在 @dw/log 和 @dw/log-upload 两个库中实施的关键性能优化,以及改造过程中遇到的技术难点和解决方案。
核心优化策略概览:
我们的优化策略主要围绕三个核心问题:
二、核心性能优化
优化一:智能化数据库清理机制
问题背景
传统日志系统的一个重大痛点是本地存储无限膨胀。用户长期使用后,IndexedDB 可能积累数万条日志记录,不仅占用大量存储空间,更拖慢了所有数据库查询和写入操作。
解决方案:双重清理策略
我们实现了一个智能清理机制,它结合了两种策略,并只在浏览器空闲时执行,避免影响正常业务。
/** * 综合清理日志(同时处理过期和数量限制) * @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) }}
复制代码
/** * 检查并执行清理(节流版本,避免频繁清理) */private checkAndCleanup = (() => { let lastCleanup = 0 const CLEANUP_INTERVAL = 5 * 60 * 1000 // 5分钟最多清理一次 return () => { const now = Date.now() if (now - lastCleanup > CLEANUP_INTERVAL) { lastCleanup = now executeWhenIdle(() => { this.performCleanup() }, 1000) } }})()
复制代码
优化二:上传模块的异步加载架构
问题背景
日志上传功能涉及 OSS 上传、文件压缩等重型依赖,如果全部打包到主库中,会显著增加包体积。更重要的是,大部分用户可能永远不会触发日志上传功能。
解决方案:动态模块加载
189KB 的包体积是不可接受的。分析发现,包含文件压缩(JSZip)和 OSS 上传的 @dw/log-upload 模块是体积元凶,但 99%的用户在正常浏览时根本用不到它。
我们采取了“核心功能+插件化”的设计思路,将非核心的上传功能彻底分离。
/** * 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 = null throw 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()操作会阻塞主线程,导致页面卡顿。
我们引入了一个日志队列,将所有日志写入请求“缓冲”起来,再由队列控制器进行优化处理。
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 = 0 this.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.jsexport 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(), ]}
复制代码
配置说明
难点二:process 对象未定义问题
问题描述
打包后的代码在浏览器环境中运行时出现 process is not defined 错误:
ReferenceError: process is not defined at Object.<anonymous> (dw-log.umd.js:1234:56)
复制代码
这通常是因为某些 Node.js 模块或工具库在代码中引用了 process 对象,而浏览器环境中并不存在。
解决方案:插件注入 process 对象
我们使用 @rollup/plugin-inject 插件,在打包时向代码中注入一个模拟的 process 对象,以满足这些库的运行时需求。
// rollup.config.jsimport 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({ // 只在需要的地方注入 process process: { 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: anytry { // 尝试 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' }), ]}
复制代码
四、性能测试与效果对比
打包优化效果对比:
五、总结
通过解决这些打包构建中的技术难点,我们不仅成功完成了日志系统的性能优化,还积累了工程化经验。这些实践不仅带来了日志系统本身的轻量化与高效化,其经验对于任何追求高性能和稳定性的前端项目都有部分参考价值。
往期回顾
得物灵犀搜索推荐词分发平台演进 3.0
R8 疑难杂症分析实战:外联优化设计缺陷引起的崩溃|得物技术
可扩展系统设计的黄金法则与 Go 语言实践|得物技术
营销会场预览直通车实践|得物技术
基于 TinyMce 富文本编辑器的客服自研知识库的技术探索和实践|得物技术
文 / 沸腾
关注得物技术,每周更新技术干货
要是觉得文章对你有帮助的话,欢迎评论转发点赞~
未经得物技术许可严禁转载,否则依法追究法律责任。
评论