一、前言

在现代前端应用中,日志回捞系统是排查线上问题的重要工具。然而,传统的日志系统往往面临着包体积过大、存储无限膨胀、性能影响用户体验等问题。本文将深入分析我们在@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://www.pswp.cn/diannao/98548.shtml
繁体地址,请注明出处:http://hk.pswp.cn/diannao/98548.shtml
英文地址,请注明出处:http://en.pswp.cn/diannao/98548.shtml

如若内容造成侵权/违法违规/事实不符,请联系英文站点网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【QT随笔】结合应用案例一文完美概括QT中的队列(Queue)

【QT随笔】结合应用案例一文完美概括QT中的队列&#xff08;Queue&#xff09; 队列&#xff08;Queue&#xff09;是一种线性数据结构&#xff0c;其核心规则为先进先出&#xff08;FIFO, First-In-First-Out&#xff09;&#xff1a; 新元素在队尾插入&#xff08;enqueue&a…

At least one <template> or <script> is required in a single file component

环境rspack vue3原因rule 中配置了两个vue-loader删掉一个即可。

LangChain实战(十八):构建ReAct模式的网页内容摘要与分析Agent

本文是《LangChain实战课》系列的第十八篇,将深入讲解如何构建一个基于ReAct模式的智能网页内容摘要与分析Agent。这个Agent能够自主浏览网页、提取关键信息、生成智能摘要,并进行深入的内容分析,让信息获取和理解变得更加高效。 前言 在信息爆炸的时代,我们每天都需要处理…

debian11 ubuntu24 armbian24 apt install pure-ftpd被动模式的正确配置方法

debian11 ubuntu24 armbian24 apt install pure-ftpd被动模式的正确配置方法 安装方法请看&#xff1a;https://www.itbulu.com/pure-ftpd.html 疑难问题解决 原本以为配置很简单的&#xff0c;无非是修改 ForcePassiveIP MinUID PassivePortRange PureDB这几个配置项就行了…

量化金融|基于算法和模型的预测研究综述

一、研究背景与发展历程​​1.​​量化投资理论演进​​•奠基阶段&#xff08;1950s-1960s&#xff09;​​&#xff1a;Markowitz均值方差理论&#xff08;1952&#xff09;、CAPM模型&#xff08;1964&#xff09;奠定现代量化投资基础•衍生品定价&#xff08;1970s-1980s&…

从零开始的云计算生活——第六十天,志在千里,使用Jenkins部署K8S

一.安装kubectl1、配置yum源cat <<EOF | tee /etc/yum.repos.d/kubernetes.repo [kubernetes] nameKubernetes baseurlhttps://mirrors.aliyun.com/kubernetes-new/core/stable/v1.28/rpm/ enabled1 gpgcheck1 gpgkeyhttps://mirrors.aliyun.com/kubernetes-new/core/sta…

无人机电压模块技术剖析

无人机电源模块的基本运行方式无人机电压模块的核心任务是对动力电源&#xff08;通常是锂电池&#xff09;进行转换、调节和分配&#xff0c;为飞控、图传、摄像头、舵机等各个子系统提供稳定可靠的电能。其运行方式可以概括为&#xff1a;电压转换与调控&#xff1a;无人机动…

MATLAB基于GM(灰色模型)与LSTM(长短期记忆网络)的组合预测方法

一、GM与LSTM的基本原理及互补性 1. GM模型的核心特点基本原理&#xff1a;通过累加生成&#xff08;AGO&#xff09;将原始无序序列转化为具有指数规律的光滑序列&#xff0c;建立一阶微分方程&#xff08;如GM(1,1)&#xff09;进行预测。其数学形式为&#xff1a; dx(1)dtax…

【菜狗每日记录】启发式算法、傅里叶变换、AC-DTC、Xmeans—20250909

&#x1f431;1、启发式算法 ① 定义 ② 特点 ③ 案例 &#x1f431;2、快速傅里叶变换FFT ① DFT离散傅里叶变换 ② FFT快速傅里叶变换 &#x1f431;3、AC-DTC聚类 &#x1f431;4、Xmeans &#x1f431;1、启发式算法 启发式算法是和最优化算法相对的。 一般而言&am…

Axure移动端选择器案例:多类型选择器设计与动态效果实现

在移动端交互设计中&#xff0c;选择器是用户输入的核心组件。Axure移动端高保真元件库提供了四种关键选择器解决方案&#xff0c;通过动态效果提升操作真实感&#xff1a; 预览地址&#xff1a;Axure 1. 基础选择器 采用底部弹窗设计&#xff0c;支持单选项快速选择。点击触发…

Spring Boot图片验证码功能实现详解 - 从零开始到完美运行

Spring Boot图片验证码功能实现详解 - 从零开始到完美运行 &#x1f4d6; 前言 大家好&#xff01;今天我要和大家分享一个非常实用的功能&#xff1a;Spring Boot图片验证码。这个功能可以防止恶意攻击&#xff0c;比如暴力破解、刷票等。我们实现的是一个带有加减法运算的图片…

HarmonyOS实现快递APP自动识别地址

​ 大家好&#xff0c;我是潘Sir&#xff0c;持续分享IT技术&#xff0c;帮你少走弯路。《鸿蒙应用开发从入门到项目实战》系列文章持续更新中&#xff0c;欢迎关注&#xff01; 随着鸿蒙&#xff08;HarmonyOS&#xff09;生态发展&#xff0c;越来越多的APP需要进行鸿蒙适…

CUDA编程13 - 测量每个Block的执行时间

一:概述 GPU 程序性能不是靠 CPU 那样的“顺序执行”来衡量的,而是靠线程块(block)和多处理器(SM)利用率。每个 block 在 GPU 的不同多处理器上执行,顺序不确定。传统的 kernel 总体计时(比如 cudaEvent 计时整个 kernel)只能知道总时间,无法分析哪个 block 慢,为什…

敏捷开发-Scrum(下)

Scrum 核心构成&#xff1a;团队、事件与工件的协同价值体系 在 Scrum 框架中&#xff0c;“团队、事件、工件” 并非孤立的模块&#xff0c;而是相互咬合的有机整体&#xff1a;Scrum 团队是价值交付的执行核心&#xff0c;Scrum 事件是节奏把控与反馈调整的机制载体&#xff…

LeetCode 单调栈 739. 每日温度

739. 每日温度给定一个整数数组 temperatures &#xff0c;表示每天的温度&#xff0c;返回一个数组 answer &#xff0c;其中 answer[i] 是指对于第 i 天&#xff0c;下一个更高温度出现在几天后。如果气温在这之后都不会升高&#xff0c;请在该位置用 0 来代替。 示例 1: 输入…

Java-面试八股文-JVM篇

JVM篇 一.在JVM中&#xff0c;什么是程序计数器? 在 JVM&#xff08;Java Virtual Machine&#xff09; 中&#xff0c;程序计数器&#xff08;Program Counter Register&#xff0c;简称 PC 寄存器&#xff09; 是一块较小的内存空间&#xff0c;用于记录 当前线程所执行的字…

微算法科技(NASDAQ: MLGO)采用量子相位估计(QPE)方法,增强量子神经网络训练

随着量子计算技术的迅猛发展&#xff0c;传统计算机在处理复杂问题时所遇到的算力瓶颈日益凸显。量子计算以其独特的并行计算能力和指数级增长的计算潜力&#xff0c;为解决这些问题提供了新的途径。微算法科技&#xff08;NASDAQ: MLGO&#xff09;探索量子技术在各种应用场景…

MySQL 备份的方法和最佳实践

MySQL 是一种流行的开源关系数据库管理系统&#xff0c;用于在线应用程序和数据仓库。它以可靠性、有效性和简单性而闻名。然而&#xff0c;与任何计算机系统一样&#xff0c;由于硬件故障、软件缺陷或其他不可预见的情况&#xff0c;存在数据丢失的可能性。因此&#xff0c;保…

应用层自定义协议、序列化和反序列化

1.自定义协议开发者根据特定应用场景的需要&#xff0c;自行设计和制定的通信规则和数据格式 1.1 核心组成部分一个典型的自定义协议通常包含以下几个关键部分&#xff1a;​帧/报文格式 (Frame/Packet Format)​​&#xff1a;定义了数据是如何打包的。这通常包括&#xff1a…

Excel VBA 中可用的工作表函数

Visual Basic for Applications (VBA) 中可用的工作表函数。可以在 VBA 中通过 Application.WorksheetFunction 对象调用。 下面我将按照字母分组&#xff0c;对每个函数进行简要解释&#xff0c;并给出在 VBA 中使用的示例。A 组Acos: 返回数字的反余弦值。 result Applicati…