HarmonyOS APP开发:TreeShaking与无用代码剔除
HarmonyOS APP开发:TreeShaking与无用代码剔除
📌 核心要点:深入理解TreeShaking的静态分析原理,掌握HarmonyOS构建系统中的TreeShaking配置,通过死代码消除和未使用模块检测实现代码体积的极致精简。
一、背景与动机
你的项目中有没有这样的代码——某个工具类写好了但从未被调用,某个第三方库引入了但只用了一个函数,某个功能模块上线后被弃用但代码还留着?
这些"僵尸代码"就像是房间角落里堆积的杂物,平时看不见,但它们实实在在地占用了空间。在HAP包中,每一行无用代码都意味着更大的字节码体积、更长的加载时间、更差的运行时内存占用。
TreeShaking(摇树优化)就是解决这个问题的利器。它的名字很形象——摇动一棵树,枯叶(无用代码)就会掉落,只留下健康的叶子(有用代码)。但TreeShaking不是简单的"删除没用的代码",它依赖于编译器的静态分析能力,需要精确判断哪些代码是"可达的"、哪些是"死代码"。
你可能会问:我手动删掉不用的代码不就行了?问题在于,现代项目的依赖关系极其复杂——一个工具函数可能被间接引用,一个看似无用的类可能通过反射被加载。手动判断"这段代码有没有被用到"几乎不可能做到准确。而TreeShaking由编译器自动完成,比人工判断更精确、更高效。
本文将带你从原理到实战,全面掌握HarmonyOS中的TreeShaking技术。
二、核心原理
2.1 TreeShaking的工作原理
flowchart TB
A[项目源代码] --> B[编译器解析入口]
B --> C[构建依赖图<br/>Dependency Graph]
C --> D[标记阶段<br/>Marking Phase]
D --> E[从入口开始遍历]
E --> F{代码是否可达?}
F -->|可达| G[✅ 标记为Live Code]
F -->|不可达| H[❌ 标记为Dead Code]
G --> I[摇树阶段<br/>Sweeping Phase]
H --> I
I --> J[保留Live Code]
I --> K[剔除Dead Code]
J --> L[生成优化后的字节码]
K --> M[记录剔除日志]
classDef entryStyle fill:#E74C3C,stroke:#C0392B,color:#fff,font-weight:bold
classDef processStyle fill:#3498DB,stroke:#2980B9,color:#fff
classDef liveStyle fill:#2ECC71,stroke:#27AE60,color:#fff,font-weight:bold
classDef deadStyle fill:#95A5A6,stroke:#7F8C8D,color:#fff
classDef outputStyle fill:#9B59B6,stroke:#8E44AD,color:#fff
class A,B entryStyle
class C,D,E,F processStyle
class G liveStyle
class H deadStyle
class I,J,K outputStyle
class L,M outputStyle
2.2 TreeShaking vs 死代码消除(DCE)
很多人把TreeShaking和DCE混为一谈,但它们其实是两个不同的优化层次:
| 对比维度 | TreeShaking | 死代码消除(DCE) |
|---|---|---|
| 优化时机 | 模块级别 | 语句/表达式级别 |
| 分析粒度 | 整个模块是否被引用 | 单条语句是否可执行 |
| 典型场景 | 未使用的import模块 | if(false)中的代码块 |
| 依赖关系 | 基于模块依赖图 | 基于控制流分析 |
| 效果 | 移除整个模块 | 移除不可达代码段 |
简单来说:TreeShaking是"大刀阔斧"地砍掉没用的模块,DCE是"精雕细琢"地剔除不可达的代码。两者配合使用,效果最佳。
2.3 HarmonyOS构建系统中的TreeShaking
HarmonyOS的ArkTS编译器(方舟编译器)在构建过程中会自动执行以下优化链路:
源代码 → 词法分析 → 语法分析 → 语义分析 → TreeShaking → DCE → 字节码生成
其中TreeShaking发生在语义分析之后、字节码生成之前,这个时机很关键——因为此时编译器已经构建了完整的依赖图,可以精确判断代码的可达性。
三、代码实战
3.1 基础示例:未使用模块检测器
在优化之前,我们需要先知道哪些代码是"死代码"。下面是一个自动检测未使用导出的工具:
// unused_detector.ets - 未使用模块与导出检测器
import { fileIo } from '@kit.CoreFileKit'
// 导出项信息
interface ExportInfo {
name: string // 导出名称
filePath: string // 所在文件路径
exportType: string // 导出类型: class/function/const/interface/enum
isUsed: boolean // 是否被引用
referencedBy: string[] // 被哪些文件引用
}
// 检测结果
interface DetectionResult {
totalExports: number // 总导出数
usedExports: number // 已使用数
unusedExports: number // 未使用数
unusedList: ExportInfo[] // 未使用列表
estimatedSavingKB: number // 预计可节省体积
}
// 未使用模块检测器
class UnusedModuleDetector {
private projectRoot: string
private exportMap: Map<string, ExportInfo> = new Map()
private importMap: Map<string, Set<string>> = new Map() // 文件 -> 引用的模块
constructor(projectRoot: string) {
this.projectRoot = projectRoot
}
// 执行检测
detect(): DetectionResult {
// 第一步:扫描所有导出
this.scanExports()
// 第二步:扫描所有引用
this.scanImports()
// 第三步:匹配引用关系
this.matchReferences()
// 第四步:生成报告
return this.generateReport()
}
// 扫描所有导出
private scanExports(): void {
const etsDir = `${this.projectRoot}/src/main/ets`
this.scanExportsInDirectory(etsDir)
}
// 递归扫描导出
private scanExportsInDirectory(dirPath: string): void {
const entries = fileIo.listFileSync(dirPath)
for (const entry of entries) {
const fullPath = `${dirPath}/${entry}`
const stat = fileIo.statSync(fullPath)
if (stat.isDirectory()) {
this.scanExportsInDirectory(fullPath)
} else if (fullPath.endsWith('.ets') || fullPath.endsWith('.ts')) {
this.extractExports(fullPath)
}
}
}
// 提取文件中的导出
private extractExports(filePath: string): void {
const content = fileIo.readTextSync(filePath)
// 匹配 export class XXX
const classPattern = /export\s+(?:default\s+)?class\s+(\w+)/g
let match: RegExpExecArray | null
while ((match = classPattern.exec(content)) !== null) {
this.addExport(match[1], filePath, 'class')
}
// 匹配 export function XXX
const funcPattern = /export\s+(?:default\s+)?function\s+(\w+)/g
while ((match = funcPattern.exec(content)) !== null) {
this.addExport(match[1], filePath, 'function')
}
// 匹配 export const/let/var XXX
const constPattern = /export\s+(?:const|let|var)\s+(\w+)/g
while ((match = constPattern.exec(content)) !== null) {
this.addExport(match[1], filePath, 'const')
}
// 匹配 export interface XXX
const interfacePattern = /export\s+interface\s+(\w+)/g
while ((match = interfacePattern.exec(content)) !== null) {
this.addExport(match[1], filePath, 'interface')
}
// 匹配 export enum XXX
const enumPattern = /export\s+enum\s+(\w+)/g
while ((match = enumPattern.exec(content)) !== null) {
this.addExport(match[1], filePath, 'enum')
}
}
// 添加导出项
private addExport(name: string, filePath: string, type: string): void {
this.exportMap.set(`${filePath}:${name}`, {
name: name,
filePath: filePath,
exportType: type,
isUsed: false,
referencedBy: []
})
}
// 扫描所有import引用
private scanImports(): void {
const etsDir = `${this.projectRoot}/src/main/ets`
this.scanImportsInDirectory(etsDir)
}
// 递归扫描import
private scanImportsInDirectory(dirPath: string): void {
const entries = fileIo.listFileSync(dirPath)
for (const entry of entries) {
const fullPath = `${dirPath}/${entry}`
const stat = fileIo.statSync(fullPath)
if (stat.isDirectory()) {
this.scanImportsInDirectory(fullPath)
} else if (fullPath.endsWith('.ets') || fullPath.endsWith('.ts')) {
this.extractImports(fullPath)
}
}
}
// 提取文件中的import
private extractImports(filePath: string): void {
const content = fileIo.readTextSync(filePath)
// 匹配 import { XXX } from 'module'
const namedImportPattern = /import\s+\{([^}]+)\}\s+from\s+['"]([^'"]+)['"]/g
let match: RegExpExecArray | null
while ((match = namedImportPattern.exec(content)) !== null) {
const names = match[1].split(',').map(n => n.trim().split(/\s+as\s+/)[0].trim())
const modulePath = match[2]
for (const name of names) {
this.recordImport(filePath, name, modulePath)
}
}
// 匹配 import XXX from 'module'
const defaultImportPattern = /import\s+(\w+)\s+from\s+['"]([^'"]+)['"]/g
while ((match = defaultImportPattern.exec(content)) !== null) {
this.recordImport(filePath, match[1], match[2])
}
}
// 记录import引用
private recordImport(fromFile: string, name: string, modulePath: string): void {
// 查找对应的导出项
this.exportMap.forEach((exportInfo, key) => {
if (exportInfo.name === name) {
exportInfo.referencedBy.push(fromFile)
}
})
}
// 匹配引用关系
private matchReferences(): void {
this.exportMap.forEach((exportInfo) => {
exportInfo.isUsed = exportInfo.referencedBy.length > 0
})
}
// 生成检测报告
private generateReport(): DetectionResult {
const allExports = Array.from(this.exportMap.values())
const unusedExports = allExports.filter(e => !e.isUsed)
const usedExports = allExports.filter(e => e.isUsed)
// 估算可节省体积(每个未使用的导出约节省2-5KB)
const estimatedSavingKB = unusedExports.reduce((sum, e) => {
const savingByType: Record<string, number> = {
'class': 5,
'function': 3,
'const': 1,
'interface': 2,
'enum': 2
}
return sum + (savingByType[e.exportType] || 2)
}, 0)
return {
totalExports: allExports.length,
usedExports: usedExports.length,
unusedExports: unusedExports.length,
unusedList: unusedExports,
estimatedSavingKB: estimatedSavingKB
}
}
// 格式化报告
formatReport(result: DetectionResult): string {
const lines: string[] = []
lines.push('🔍 未使用模块检测报告')
lines.push('='.repeat(50))
lines.push(`总导出数: ${result.totalExports}`)
lines.push(`已使用: ${result.usedExports} ✅`)
lines.push(`未使用: ${result.unusedExports} ❌`)
lines.push(`预计可节省: ~${result.estimatedSavingKB}KB`)
lines.push('')
if (result.unusedList.length > 0) {
lines.push('未使用导出列表:')
for (const item of result.unusedList) {
lines.push(` ❌ ${item.exportType} ${item.name}`)
lines.push(` 文件: ${this.getRelativePath(item.filePath)}`)
}
}
return lines.join('\n')
}
private getRelativePath(fullPath: string): string {
const idx = fullPath.indexOf('/ets/')
return idx >= 0 ? fullPath.substring(idx + 1) : fullPath
}
}
// 使用示例
function detectUnusedModules() {
const detector = new UnusedModuleDetector('/path/to/entry')
const result = detector.detect()
console.info(detector.formatReport(result))
}
3.2 进阶示例:TreeShaking配置与调优
// treeshaking_config.ets - TreeShaking配置与调优
import { hapBuildProfile } from '@ohos/hvigor'
// TreeShaking配置选项
interface TreeShakingConfig {
// 基础开关
enableTreeShaking: boolean // 启用TreeShaking
enableDCE: boolean // 启用死代码消除
// 高级选项
aggressiveMode: boolean // 激进模式(更激进的代码剔除)
keepSideEffects: boolean // 保留副作用模块
analyzeOnly: boolean // 仅分析不删除
// 保留规则
keepModules: string[] // 保留的模块路径
keepExports: string[] // 保留的导出名称
keepFiles: string[] // 保留的文件路径
// 日志选项
verboseLog: boolean // 详细日志
outputUnusedReport: boolean // 输出未使用代码报告
}
// 默认配置
const DEFAULT_TREESHAKING_CONFIG: TreeShakingConfig = {
enableTreeShaking: true,
enableDCE: true,
aggressiveMode: false,
keepSideEffects: true,
analyzeOnly: false,
keepModules: [],
keepExports: [],
keepFiles: [],
verboseLog: false,
outputUnusedReport: true
}
// TreeShaking配置管理器
class TreeShakingConfigManager {
private config: TreeShakingConfig
constructor(config?: Partial<TreeShakingConfig>) {
this.config = { ...DEFAULT_TREESHAKING_CONFIG, ...config }
}
// 生成构建配置
generateBuildConfig(): Record<string, Object> {
return {
'arkCompilerOptions': {
// TreeShaking开关
'enableTreeShaking': this.config.enableTreeShaking,
// 死代码消除
'enableDCE': this.config.enableDCE,
// 激进模式
'aggressiveOptimization': this.config.aggressiveMode,
// 副作用分析
'sideEffectAnalysis': this.config.keepSideEffects ? 'conservative' : 'aggressive',
// 仅分析模式
'analyzeOnly': this.config.analyzeOnly
},
// 保留规则
'keepRules': {
'keepModules': this.config.keepModules,
'keepExports': this.config.keepExports,
'keepFiles': this.config.keepFiles
},
// 日志配置
'logging': {
'verbose': this.config.verboseLog,
'outputUnusedReport': this.config.outputUnusedReport
}
}
}
// 分析TreeShaking效果
analyzeShakingEffect(buildOutputDir: string): TreeShakingReport {
const report: TreeShakingReport = {
originalSizeKB: 0,
optimizedSizeKB: 0,
reductionPercent: 0,
removedModules: [],
keptModules: [],
sideEffectModules: []
}
// 读取TreeShaking日志
const logPath = `${buildOutputDir}/treeshaking-report.json`
try {
const content = fileIo.readTextSync(logPath)
const logData = JSON.parse(content)
report.originalSizeKB = logData.originalSizeKB || 0
report.optimizedSizeKB = logData.optimizedSizeKB || 0
report.reductionPercent = logData.originalSizeKB > 0
? ((1 - logData.optimizedSizeKB / logData.originalSizeKB) * 100)
: 0
report.removedModules = logData.removedModules || []
report.keptModules = logData.keptModules || []
report.sideEffectModules = logData.sideEffectModules || []
} catch (e) {
console.warn('TreeShaking日志不存在,请先执行构建')
}
return report
}
// 生成优化建议
generateSuggestions(report: TreeShakingReport): string[] {
const suggestions: string[] = []
// 检查副作用模块
if (report.sideEffectModules.length > 0) {
suggestions.push(`发现${report.sideEffectModules.length}个副作用模块未被TreeShaking移除`)
suggestions.push('建议检查这些模块是否真的有副作用,如果是纯函数模块,可以标记为"sideEffects: false"')
for (const mod of report.sideEffectModules.slice(0, 5)) {
suggestions.push(` - ${mod}`)
}
}
// 检查优化效果
if (report.reductionPercent < 10) {
suggestions.push(`TreeShaking仅减少了${report.reductionPercent.toFixed(1)}%的代码体积,效果不理想`)
suggestions.push('建议检查是否存在以下问题:')
suggestions.push(' 1. 过多的全局变量或顶层副作用代码')
suggestions.push(' 2. 动态import或反射导致无法静态分析')
suggestions.push(' 3. 第三方库未标记sideEffects')
}
// 检查保留规则是否过多
if (this.config.keepExports.length > 20) {
suggestions.push(`保留了${this.config.keepExports.length}个导出名称,可能过于保守`)
suggestions.push('建议逐一审查保留规则,移除不必要的保留项')
}
return suggestions
}
}
// TreeShaking报告
interface TreeShakingReport {
originalSizeKB: number
optimizedSizeKB: number
reductionPercent: number
removedModules: string[]
keptModules: string[]
sideEffectModules: string[]
}
// 使用示例
function configureTreeShaking() {
const manager = new TreeShakingConfigManager({
enableTreeShaking: true,
enableDCE: true,
aggressiveMode: false,
keepSideEffects: true,
keepExports: ['EntryAbility', 'MainAbility'],
verboseLog: true,
outputUnusedReport: true
})
// 生成构建配置
const buildConfig = manager.generateBuildConfig()
console.info('TreeShaking构建配置:', JSON.stringify(buildConfig, null, 2))
// 分析效果
const report = manager.analyzeShakingEffect('/path/to/build/output')
console.info(`TreeShaking效果: 减少${report.reductionPercent.toFixed(1)}%代码体积`)
// 生成建议
const suggestions = manager.generateSuggestions(report)
for (const s of suggestions) {
console.info(s)
}
}
3.3 完整示例:死代码消除与副作用标记
// dce_optimizer.ets - 死代码消除与副作用标记优化
import { fileIo } from '@kit.CoreFileKit'
// 副作用分析结果
interface SideEffectAnalysis {
filePath: string // 文件路径
hasSideEffects: boolean // 是否有副作用
sideEffectTypes: string[] // 副作用类型
canMarkPure: boolean // 是否可以标记为纯模块
}
// DCE优化器
class DCEOptimizer {
private projectRoot: string
private analysisResults: SideEffectAnalysis[] = []
constructor(projectRoot: string) {
this.projectRoot = projectRoot
}
// 执行完整的DCE优化流程
async optimize(): Promise<void> {
// 第一步:分析副作用
this.analyzeSideEffects()
// 第二步:标记纯模块
this.markPureModules()
// 第三步:消除死代码
this.eliminateDeadCode()
// 第四步:生成报告
this.generateReport()
}
// 分析副作用
private analyzeSideEffects(): void {
const etsDir = `${this.projectRoot}/src/main/ets`
this.analyzeDirectory(etsDir)
}
// 递归分析目录
private analyzeDirectory(dirPath: string): void {
const entries = fileIo.listFileSync(dirPath)
for (const entry of entries) {
const fullPath = `${dirPath}/${entry}`
const stat = fileIo.statSync(fullPath)
if (stat.isDirectory()) {
this.analyzeDirectory(fullPath)
} else if (fullPath.endsWith('.ets') || fullPath.endsWith('.ts')) {
this.analyzeFile(fullPath)
}
}
}
// 分析单个文件的副作用
private analyzeFile(filePath: string): void {
const content = fileIo.readTextSync(filePath)
const sideEffectTypes: string[] = []
// 检测顶层副作用代码
// 1. 顶层赋值(非const声明)
if (/^(?:let|var)\s+\w+\s*=/.test(content)) {
sideEffectTypes.push('顶层可变变量赋值')
}
// 2. 顶层函数调用
const topLevelCallPattern = /^[a-zA-Z_$][\w$]*\s*\(/gm
if (topLevelCallPattern.test(content)) {
sideEffectTypes.push('顶层函数调用')
}
// 3. 全局对象访问(AppStorage, globalThis等)
if (/AppStorage\.(?:set|get)|globalThis\./.test(content)) {
sideEffectTypes.push('全局状态访问')
}
// 4. 模块副作用(console, 日志等)
if (/^console\./gm.test(content)) {
sideEffectTypes.push('console输出')
}
// 5. 原型修改
if (/\.prototype\s*=|Object\.defineProperty/.test(content)) {
sideEffectTypes.push('原型修改')
}
// 6. 网络请求
if (/http\.(?:request|get|post)|@ohos\.net\.http/.test(content)) {
sideEffectTypes.push('网络请求')
}
const hasSideEffects = sideEffectTypes.length > 0
// 判断是否可以标记为纯模块
const canMarkPure = !hasSideEffects ||
(sideEffectTypes.length === 1 && sideEffectTypes[0] === 'console输出')
this.analysisResults.push({
filePath: filePath,
hasSideEffects: hasSideEffects,
sideEffectTypes: sideEffectTypes,
canMarkPure: canMarkPure
})
}
// 标记纯模块
private markPureModules(): void {
// 在package.json中添加sideEffects字段
const packageJsonPath = `${this.projectRoot}/package.json`
const pureModules: string[] = []
for (const result of this.analysisResults) {
if (result.canMarkPure) {
// 转换为相对路径
const relativePath = result.filePath.replace(this.projectRoot, '.')
pureModules.push(relativePath)
}
}
try {
const content = fileIo.readTextSync(packageJsonPath)
const packageJson = JSON.parse(content)
// 添加sideEffects配置
packageJson.sideEffects = false // 默认所有模块无副作用
// 如果有必须保留副作用的模块,使用数组形式
const nonPureModules = this.analysisResults
.filter(r => !r.canMarkPure && r.hasSideEffects)
.map(r => r.filePath.replace(this.projectRoot, '.'))
if (nonPureModules.length > 0) {
packageJson.sideEffects = nonPureModules
}
fileIo.writeTextSync(packageJsonPath, JSON.stringify(packageJson, null, 2))
console.info(`已更新sideEffects配置: ${pureModules.length}个纯模块, ${nonPureModules.length}个副作用模块`)
} catch (e) {
console.warn('更新package.json失败:', e)
}
}
// 消除死代码
private eliminateDeadCode(): void {
for (const result of this.analysisResults) {
if (!result.canMarkPure) continue
const content = fileIo.readTextSync(result.filePath)
let optimized = content
// 消除条件为false的代码块
optimized = this.removeFalseConditionBlocks(optimized)
// 消除不可达的return后代码
optimized = this.removeUnreachableCode(optimized)
// 消除空函数体
optimized = this.removeEmptyFunctions(optimized)
// 仅在内容变化时写回
if (optimized !== content) {
fileIo.writeTextSync(result.filePath, optimized)
}
}
}
// 移除条件为false的代码块
private removeFalseConditionBlocks(code: string): string {
// 移除 if (false) { ... } 块
return code.replace(/if\s*\(\s*false\s*\)\s*\{[^}]*\}/g, '')
}
// 移除不可达代码
private removeUnreachableCode(code: string): string {
// 移除return语句后的代码
const lines = code.split('\n')
const result: string[] = []
let afterReturn = false
for (const line of lines) {
if (afterReturn) {
// 检查是否进入了新的函数或代码块
if (/^\s*(function|export|class|@Component|@Entry)/.test(line)) {
afterReturn = false
} else {
continue // 跳过不可达代码
}
}
if (/^\s*return\s/.test(line) || /^\s*return;/.test(line)) {
afterReturn = true
}
result.push(line)
}
return result.join('\n')
}
// 移除空函数体
private removeEmptyFunctions(code: string): string {
// 仅移除注释标记为可删除的空函数
return code.replace(/\/\/\s*@dead-code[\s\S]*?function\s+\w+\s*\([^)]*\)\s*\{\s*\}/g, '')
}
// 生成报告
private generateReport(): void {
const total = this.analysisResults.length
const withSideEffects = this.analysisResults.filter(r => r.hasSideEffects).length
const pureModules = this.analysisResults.filter(r => r.canMarkPure).length
console.info('📊 DCE优化报告')
console.info('='.repeat(40))
console.info(`总文件数: ${total}`)
console.info(`有副作用: ${withSideEffects}`)
console.info(`纯模块: ${pureModules}`)
console.info(`TreeShaking可优化: ${pureModules}个文件`)
}
}
// 使用示例
async function runDCEOptimization() {
const optimizer = new DCEOptimizer('/path/to/entry')
await optimizer.optimize()
}
四、踩坑与注意事项
坑点1:副作用模块被误剔除
这是TreeShaking最危险的坑。有些模块看起来没有导出被引用,但它们在加载时会执行一些初始化操作(如注册全局组件、修改原型链、设置全局配置)。如果TreeShaking把这样的模块当作"未使用"移除了,运行时就会出现莫名其妙的错误。务必在package.json中正确配置sideEffects字段,标记有副作用的模块。
坑点2:动态import导致TreeShaking失效
TreeShaking依赖静态分析,而import()动态导入在运行时才确定加载哪个模块。如果你的代码中大量使用动态import,TreeShaking就无法确定哪些模块是可达的,只能保守地保留所有可能被动态加载的模块。尽量使用静态import,仅在确实需要按需加载时才使用动态import。
坑点3:第三方库未标记sideEffects
很多第三方库的package.json中没有sideEffects字段,TreeShaking只能保守地假设所有模块都有副作用,导致无法移除未使用的模块。优先选择支持TreeShaking的库(在package.json中声明了sideEffects: false),或者手动在构建配置中指定第三方库的副作用规则。
坑点4:全局变量和顶层代码阻碍TreeShaking
ArkTS中的顶层代码(不在任何函数内的代码)会被编译器视为"必须执行"的代码,即使这些代码的执行结果从未被使用。避免在模块顶层编写复杂的初始化逻辑,将它们封装到函数中,由调用方决定是否执行。
坑点5:混淆与TreeShaking的执行顺序冲突
代码混淆和TreeShaking都在编译阶段执行,如果执行顺序不当,可能导致TreeShaking误判。HarmonyOS的编译器会先执行TreeShaking再执行混淆,这个顺序是正确的。但如果你在混淆规则中使用了通配符(如-keep-property-name *),可能会保留大量本应被TreeShaking移除的代码。混淆的keep规则要尽量精确,避免使用通配符。
坑点6:接口类型声明被误删
TypeScript的interface声明在编译后不会产生运行时代码,但如果你的代码通过反射或序列化依赖这些接口的名称,TreeShaking可能会移除它们。参与序列化的接口名需要加入混淆keep规则,确保名称信息在运行时可用。
坑点7:TreeShaking日志不够详细
默认情况下,TreeShaking只输出简要的优化统计信息,不会告诉你具体移除了哪些代码。如果需要详细的日志,需要在构建配置中开启verboseLog选项。建议在CI/CD流水线中始终开启详细日志,便于排查TreeShaking导致的问题。
五、HarmonyOS 6适配说明
API差异表
| 功能/接口 | HarmonyOS 5 | HarmonyOS 6 | 变更说明 |
|---|---|---|---|
| TreeShaking | 模块内TreeShaking | 跨模块TreeShaking | 支持HSP模块间的代码剔除 |
| DCE | 基础DCE | 增强DCE | 支持更精确的控制流分析 |
| 副作用分析 | 手动配置sideEffects | 自动副作用推断 | 编译器自动分析模块副作用 |
| TreeShaking日志 | 简要统计 | 详细JSON报告 | 包含每个模块的保留/移除决策 |
| 构建缓存 | 全量重建 | 增量TreeShaking | 仅重新分析变更的模块 |
行为变更
-
跨模块TreeShaking:HarmonyOS 6的TreeShaking可以跨越HSP模块边界,移除HSP中未被主模块使用的导出。这意味着HSP模块的体积也会因主模块的使用方式而变化。
-
自动副作用推断:编译器会自动分析模块的顶层代码,判断是否存在副作用,不再完全依赖
sideEffects字段。但如果手动配置了sideEffects,优先级高于自动推断。 -
增量TreeShaking:当仅修改了部分源文件时,构建系统只会重新分析受影响的模块,大幅缩短构建时间。
适配代码
// HarmonyOS 6 TreeShaking配置 - build-profile.json5
{
"module": {
"name": "entry",
"type": "entry",
"arkCompilerOptions": {
// HarmonyOS 6增强TreeShaking
"enableTreeShaking": true,
"enableDCE": true,
// 跨模块TreeShaking
"crossModuleTreeShaking": true,
// 自动副作用推断
"autoSideEffectInference": true,
// 增量TreeShaking
"incrementalTreeShaking": true,
// 详细日志
"treeShakingReport": true
}
}
}
// HarmonyOS 6 package.json sideEffects配置
{
"name": "entry",
"version": "1.0.0",
"main": "",
"sideEffects": [
// 仅有副作用的模块路径
"./src/main/ets/init/AppInit.ets",
"./src/main/ets/utils/ThirdPartySetup.ets"
// 未列出的模块自动视为纯模块,可被TreeShaking移除
]
}
六、总结
三维度评价表
| 评价维度 | 评分 | 说明 |
|---|---|---|
| 技术深度 | ⭐⭐⭐⭐⭐ | 从TreeShaking原理到DCE算法,从副作用分析到跨模块优化,全面覆盖 |
| 实战价值 | ⭐⭐⭐⭐⭐ | 提供了未使用模块检测器、TreeShaking配置管理器和DCE优化器三大工具 |
| 适配前瞻 | ⭐⭐⭐⭐ | 详解了HarmonyOS 6的跨模块TreeShaking和自动副作用推断特性 |
TreeShaking是包体积优化中最"自动化"的手段——只要配置正确,编译器就会帮你自动剔除无用代码。但"配置正确"这四个字,恰恰是最难的部分。建议在项目中建立TreeShaking检查清单:确保所有模块正确标记sideEffects、避免不必要的动态import、定期审查keep规则。让编译器帮你"摇树",但你要确保摇的是正确的树。
- 点赞
- 收藏
- 关注作者
评论(0)