
HarmonyOS 包体积优化实战从 HAP 变大到资源账本、模块拆分和构建守门很多 HarmonyOS 项目的包体积不是突然失控的。它通常是这样慢慢长大的一次需求加了几张高清引导图下一次活动页带进一批静态 JSON再后来某个小功能引入了一个大依赖。等到临近上架团队才发现 HAP 明显变重下载、安装和审核材料都变得被动。包体积优化不能只靠最后一天“删几张图”。更稳的做法是把包体积当成工程资产来管理先知道大在哪里再区分资源、代码、依赖和模块产物最后把检查动作放进构建和发版流程里。这篇文章围绕一个具体目标展开让一个 Stage 模型 HarmonyOS 工程具备可复盘的包体积治理流程。你可以直接照着做四件事建立资源账本知道图片、字体、JSON、rawfile 分别占了多少。给新增资源和依赖加 owner避免“谁都不知道是谁加的”。按 HAP、HAR、HSP 的边界判断哪些能力适合拆出去。在构建阶段生成体积报告发现异常增长时提前拦住。一、先定边界本文优化的是安装包体积不是运行内存包体积优化和运行性能优化容易混在一起。图片压缩可以同时影响包体积和运行内存但两者的分析入口不同。维度本文关注暂不展开安装包大小HAP、APP、资源、依赖、模块产物运行时内存峰值工程结构HAP、HAR、HSP、AppScope 资源页面渲染帧率构建检查构建产物扫描、体积差异报告后端接口耗时发版风险上架前包体积异常增长页面启动链路优化HarmonyOS 应用包结构里HAP 是应用安装和运行的基本单元HAR 常用于静态共享代码和资源HSP 是动态共享包可以在模块间共享代码和资源。体积治理时要先弄清楚资源最终进了哪个产物而不是只盯着工程目录。本文示例面向 HarmonyOS NEXT / Stage 模型 / ArkTS 工程。不同项目的 DevEco Studio、SDK、API 版本可能不同落地时以你本地构建产物为准。二、先跑一次基线没有基线就没有优化不要一上来删资源。第一步应该先生成一个“优化前基线”记录当前包体积分布。建议记录下面这些数据项目示例用途构建模式releasedebug 产物通常不能代表真实发布大小HAP 大小42.6 MB判断主包是否超预期图片资源大小18.2 MB判断是否图片过重rawfile 大小6.4 MB排查静态 JSON、模型、音频等资源三方依赖大小8.1 MB判断小功能是否引入大库最近版本对比4.7 MB发现异常增长来源如果团队没有历史报告可以从当前版本开始补。哪怕第一版只有总大小也比完全没有基线好。包体积治理最怕的是“感觉这次好像变大了”而不是“这次比上次大了 4.7 MB其中图片多了 3.1 MB”。三、资料与工程位置体积问题通常藏在这几类目录里实际排查时优先看下面几个位置位置常见问题排查重点AppScope/resources全局资源被所有模块打包使用是否放了只属于某个页面的图entry/src/main/resources图片、字体、rawfile 过大是否有未使用资源或重复资源feature_x/src/main/resources功能模块资源随模块进入产物是否应该做按需模块oh-package.json5依赖不断增加是否存在无 owner 的大依赖hvigorfile.ts构建没有体积检查是否能输出报告并比较阈值一个常见误区是只看resources/base/media。很多项目真正变大的地方反而是rawfile临时 JSON、离线包、测试数据、模型文件、音频文件都可能被放进去。只要它进入构建产物就会影响包体积。四、资源账本先把“谁占空间”说清楚下面这段脚本用于扫描资源目录按类型统计大小。它不是替代官方构建工具而是给团队一个可读的账本。importfsfromfs;importpathfrompath;typeResourceTypeimage|font|json|audio|raw|other;interfaceResourceSizeItem{filePath:string;type:ResourceType;sizeKb:number;}constimageExtnewSet([.png,.jpg,.jpeg,.webp,.svg]);constfontExtnewSet([.ttf,.otf]);constaudioExtnewSet([.mp3,.wav,.aac]);functiondetectType(filePath:string):ResourceType{constextpath.extname(filePath).toLowerCase();if(imageExt.has(ext)){returnimage;}if(fontExt.has(ext)){returnfont;}if(audioExt.has(ext)){returnaudio;}if(ext.json){returnjson;}if(filePath.includes(${path.sep}rawfile${path.sep})){returnraw;}returnother;}functionscanResources(rootDir:string):ResourceSizeItem[]{constresult:ResourceSizeItem[][];functionwalk(current:string):void{constentriesfs.readdirSync(current,{withFileTypes:true});for(constentryofentries){constfullPathpath.join(current,entry.name);if(entry.isDirectory()){walk(fullPath);continue;}conststatfs.statSync(fullPath);result.push({filePath:fullPath,type:detectType(fullPath),sizeKb:Number((stat.size/1024).toFixed(2))});}}walk(rootDir);returnresult;}这段代码的边界很清楚它只扫描资源文件不判断运行时是否真的使用。detectType()先按扩展名归类再单独识别rawfile。扫描结果可以用于生成报告也可以接到 CI 中做阈值检查。如果你的项目里还有模型文件、地图包、离线数据包可以继续扩展ResourceType。有了账本之后再决定压缩、删除或拆分。不要在不知道体积分布的情况下凭感觉删文件。五、生成报告让体积变化能被复盘资源扫描结果需要汇总成报告。报告不需要很复杂但必须能回答三个问题本次总大小是多少、最大资源是哪几类、和上一次相比增长了多少。interfacePackageSizeSummary{version:string;totalKb:number;imageKb:number;fontKb:number;jsonKb:number;audioKb:number;rawKb:number;otherKb:number;topFiles:ResourceSizeItem[];}functionbuildSummary(version:string,items:ResourceSizeItem[]):PackageSizeSummary{constsummary:PackageSizeSummary{version,totalKb:0,imageKb:0,fontKb:0,jsonKb:0,audioKb:0,rawKb:0,otherKb:0,topFiles:[]};for(constitemofitems){summary.totalKbitem.sizeKb;if(item.typeimage){summary.imageKbitem.sizeKb;}elseif(item.typefont){summary.fontKbitem.sizeKb;}elseif(item.typejson){summary.jsonKbitem.sizeKb;}elseif(item.typeaudio){summary.audioKbitem.sizeKb;}elseif(item.typeraw){summary.rawKbitem.sizeKb;}else{summary.otherKbitem.sizeKb;}}summary.topFiles[...items].sort((a,b)b.sizeKb-a.sizeKb).slice(0,20);returnsummary;}代码解释PackageSizeSummary不是构建产物格式而是团队内部的体积账本。topFiles保留前 20 个大文件方便评审时直接定位。报告按版本保存才能比较“这次为什么变大”。如果后续接入 HAP 解包扫描可以把 HAP 内部文件也喂给同一个汇总函数。建议把报告保存为package-size-report.json并在发版记录里保留一份。下一次体积反弹时不需要靠记忆排查。六、图片资源按展示场景准备不要把原图塞进包里图片通常是包体积增长最快的来源。尤其是启动页、引导页、活动页、空状态图很容易把设计源图直接放进工程。治理图片时先按展示场景分层场景建议不建议小图标使用合适尺寸或矢量资源放 2K 位图列表缩略图按卡片实际显示尺寸准备用详情页大图复用启动/引导图控制分辨率和数量多套未使用引导图同时保留活动页素材活动结束后及时清理长期留在正式资源目录可以给图片压缩策略写一个计划生成函数interfaceImageCompressPlan{source:string;displayWidthVp:number;displayHeightVp:number;targetWidthPx:number;targetHeightPx:number;quality:number;}functioncreateImagePlan(source:string,widthVp:number,heightVp:number):ImageCompressPlan{constscale3;consttargetWidthPxMath.ceil(widthVp*scale);consttargetHeightPxMath.ceil(heightVp*scale);return{source,displayWidthVp:widthVp,displayHeightVp:heightVp,targetWidthPx,targetHeightPx,quality:targetWidthPx1200?82:88};}这段代码表达的是策略不是强制所有项目都按 3 倍图处理。你可以根据目标设备、设计规范和视觉验收调整scale。重点是图片进入工程前要知道它服务于哪个展示尺寸而不是把设计原图直接打包。七、rawfile 和 JSON测试数据最容易混进正式包很多团队体积异常增长不是因为图片而是因为rawfile里混入了测试资源。例如mock 接口响应 JSON。临时调试日志。离线地图包或模型文件。设计标注图和验收截图。活动页下线后遗留的配置文件。可以建立一个发布白名单只允许明确声明用途的 rawfile 进入正式包interfaceRawFileRule{path:string;owner:string;reason:string;allowRelease:boolean;}functionfindReleaseRiskRawFiles(rules:RawFileRule[]):RawFileRule[]{returnrules.filter((item){constmissingOwneritem.owner.trim().length0;constmissingReasonitem.reason.trim().length0;returnitem.allowRelease(missingOwner||missingReason);});}这段代码防的不是技术错误而是流程漏洞owner为空时后续没人负责解释这个文件为什么存在。reason为空时评审无法判断它是否应该进正式包。allowRelease明确区分调试资源和发布资源。这类规则适合放在发版检查里而不是靠人工记忆。如果你的项目里有离线资源建议再加一列expireVersion活动结束或版本过期后自动提示清理。八、模块拆分不是所有能力都应该塞进 entry包体积优化不是简单拆模块。拆错了会让启动链路更复杂甚至引入新的加载问题。判断一个能力是否适合从主 HAP 中拆出去至少看三个条件判断点适合拆分不适合拆分是否首屏必需否是是否高频使用低频高频是否资源重图片、模型、离线包较多代码和资源都很轻下面是一个模块评估模型interfaceFeatureModuleSize{name:string;requiredAtStartup:boolean;monthlyUsagePercent:number;resourceKb:number;dependencyKb:number;}functionsuggestSplitModules(modules:FeatureModuleSize[]):FeatureModuleSize[]{returnmodules.filter((module){constnotStartupRequired!module.requiredAtStartup;constlowFrequencymodule.monthlyUsagePercent20;constheavyEnoughmodule.resourceKbmodule.dependencyKb1024;returnnotStartupRequiredlowFrequencyheavyEnough;});}代码解释requiredAtStartup保护启动主路径避免把启动必需能力拆走。monthlyUsagePercent用来衡量功能频率不能只凭产品印象。resourceKb dependencyKb表示拆分收益太小的模块不值得增加复杂度。这个函数只给建议真正拆分还要看业务路由、权限、测试覆盖和上架策略。HarmonyOS 工程中HAR 和 HSP 的使用边界要结合官方程序包结构理解。HAR 是静态共享包常用于共享代码和资源HSP 是动态共享包适合共享能力和资源。不要为了“看起来模块化”盲目拆分。九、依赖治理小功能引大库是体积杀手依赖问题很隐蔽因为它通常不会出现在页面代码里。一个看似很小的功能可能引入一套大库、Native 产物或重复资源。建议在oh-package.json5旁边维护一份依赖登记表interfaceDependencyRecord{name:string;version:string;owner:string;reason:string;removable:boolean;sizeKb:number;}functionfindSuspiciousDependencies(records:DependencyRecord[]):DependencyRecord[]{returnrecords.filter((item){constnoReasonitem.reason.trim().length0;constnoOwneritem.owner.trim().length0;constlargeDependencyitem.sizeKb500;returnlargeDependency(noReason||noOwner||item.removable);});}这段代码关注三个风险大依赖没有引入理由。大依赖没有 owner。依赖已经可移除但仍留在工程里。依赖治理不要只在发版前做。最好在代码评审时就要求说明为什么需要这个依赖、是否有轻量替代、是否只在某个可选模块使用。十、构建守门让异常增长提前暴露体积报告真正有用是因为它可以和上一版比较。下面的比较函数可以作为 CI 守门逻辑的核心。interfaceSizeDiffWarning{level:info|warning|block;message:string;}functioncomparePackageSize(previous:PackageSizeSummary,current:PackageSizeSummary):SizeDiffWarning[]{constwarnings:SizeDiffWarning[][];constdiffKbcurrent.totalKb-previous.totalKb;if(diffKb2048){warnings.push({level:block,message:本次资源体积增长${diffKb.toFixed(2)}KB超过 2MB需要说明原因});}elseif(diffKb1024){warnings.push({level:warning,message:本次资源体积增长${diffKb.toFixed(2)}KB建议复查新增资源});}if(current.imageKb-previous.imageKb1024){warnings.push({level:warning,message:图片资源增长超过 1MB请检查新增引导图、活动图和空状态图});}returnwarnings;}阈值不要照抄。工具类应用、内容型应用、离线资源型应用的包体积策略不一样。建议从宽松阈值开始例如单版本增长超过 2MB 必须说明原因等团队习惯稳定后再逐步收紧。十一、落地到工程一套可执行的检查命令可以把扫描脚本放在tools/package-size并给它一个固定入口tools/package-size/ ├── scan-resources.ts ├── build-summary.ts ├── compare-report.ts └── package-size-baseline.json本地执行流程可以这样设计hvigor assembleHap--modereleasenodetools/package-size/scan-resources.js ./entry/src/main/resources ./reports/package-size-current.jsonnodetools/package-size/compare-report.js ./reports/package-size-baseline.json ./reports/package-size-current.json如果你使用的是 DevEco Studio 图形界面构建也可以先手动构建 HAP再运行扫描脚本。关键不在于命令完全一致而在于每次发版都能生成同一种格式的报告。十二、常见问题排查按增长来源找不要全工程乱翻现象优先怀疑检查方法修复方向HAP 突然增加几 MB新增图片、rawfile 或依赖比较两版 topFiles删除未使用资源或压缩大图图片压缩后变糊压缩策略不分场景对比实际显示尺寸图标、照片、引导图分策略活动结束后包体积没降活动资源未清理查活动目录和 rawfile加资源过期版本拆模块后启动失败主路径依赖被拆走检查启动页依赖链启动必需能力留在主模块依赖越来越多没有 owner 和引入理由检查依赖登记表无理由大依赖进入评审发版前才发现异常构建阶段没有对比报告查 CI 或发版清单加体积报告和阈值提醒排查顺序建议固定先看版本差异再看资源分类最后看模块和依赖。不要一上来全工程搜索“哪个文件大”那样效率很低也容易误删业务仍在使用的资源。十三、发布前验收清单合入或发版前至少确认下面这些项检查项通过标准有优化前后报告能看到总大小和分类大小变化topFiles 已复查前 20 个大文件都有用途说明rawfile 已清理mock、临时截图、调试 JSON 没进正式包图片按场景处理图标、照片、引导图没有使用同一压缩策略大依赖有 owner每个大依赖都有引入理由和负责人可选模块已评估非首屏、低频、大资源能力有拆分结论构建守门已执行单版本异常增长会被提示或阻断验收清单的目的不是增加流程负担而是防止问题拖到上架前爆雷。包体积越早被看见修复成本越低。十四、给团队的推进节奏如果项目已经比较大不建议一次性做彻底重构。可以按三周推进周期目标产出第 1 周建账本资源扫描脚本、当前版本报告第 2 周清高收益资源大图、rawfile、过期活动资源清理记录第 3 周上构建守门基线报告、增长阈值、发版检查项这个节奏比较适合真实团队第一周先建立可见性第二周处理最明显收益第三周把流程固定住。只要流程固定后面的优化就不会变成一次性的“清仓大扫除”。十五、本文参考的官方资料华为开发者文档应用包体积优化https://developer.huawei.com/consumer/cn/doc/best-practices/bpta-decrease_pakage_size华为开发者文档应用程序包基础知识https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/application-package-overview华为开发者文档应用程序包结构https://developer.huawei.com/consumer/cn/doc/HarmonyOS-Guides/application-package-structure-stage华为开发者文档HARhttps://developer.huawei.com/consumer/cn/doc/HarmonyOS-Guides/har-package华为开发者文档打包工具https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/packing-tool十六、最后把包体积变成日常工程指标HarmonyOS 包体积优化的关键不是最后一天临时删文件而是建立一套能持续复盘的工程机制。先用资源账本看清楚体积来源再按图片、rawfile、依赖、模块拆分逐层治理最后把报告和阈值放进构建流程。这样每一次体积增长都有原因每一次优化都有记录包体积就不会失控到上架前才被发现。最后可以用这张表判断团队是否已经进入稳定状态判断问题稳定状态这次版本为什么变大能从报告里说出增长来源哪些资源可以删topFiles 和 rawfile 清单里有结论大依赖是谁引入的依赖登记表里有 owner 和理由下次还会不会反弹CI 或发版检查会提示异常增长新人接手能不能继续维护账本、阈值、验收清单都有文档如果这五个问题都能回答包体积治理就不再依赖某一个人的经验而是变成团队日常工程质量的一部分。