• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# 延迟加载(lazy import)
2<!--Kit: ArkTS-->
3<!--Subsystem: ArkCompiler-->
4<!--Owner: @DaiHuina1997-->
5<!--Designer: @yao_dashuai-->
6<!--Tester: @kirl75; @zsw_zhushiwei-->
7<!--Adviser: @foryourself-->
8
9随着应用程序功能的扩展,冷启动时间显著增加,主要是因为启动初期加载了大量未实际执行的模块。这不仅延长了应用的初始化时间,还浪费了资源。需要精简加载流程,剔除非必需的文件执行,优化冷启动性能,确保用户体验流畅。
10
11> **说明:**
12>
13> - 延迟加载特性在API 12版本开始支持。
14>
15> - 开发者如需在API 12上使用lazy import语法,需在工程中配置"compatibleSdkVersionStage": "beta3",否则将无法通过编译。参考[DevEco Studio build-profile.json5配置文件说明](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/ide-hvigor-build-profile-V5#section511142752919)16> - 针对API version大于12的工程,开发者可直接使用lazy import语法,无需再进行其他配置。
17
18## 功能特性
19
20延迟加载特性使文件在冷启动阶段不被加载,而是在程序运行时按需加载,从而缩短冷启动时间。
21
22## 使用方式
23
24开发者可以利用[DevEco Profiler展示冷启动过程文件加载情况](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/ide-insight-session-launch)、[可延迟加载文件检测](#可延迟加载文件检测)、<!--Del-->[<!--DelEnd-->Trace<!--Del-->](../performance/common-trace-using-instructions.md)<!--DelEnd-->工具或日志记录等手段,识别冷启动期间未被实际调用的文件<!--RP1-->,分析方法可参考[可延迟加载文件检测](#可延迟加载文件检测)<!--RP1End-->。通过对这些数据的分析,开发者可以精准定位启动阶段不必预先加载的文件列表,并在这些文件的调用点增加lazy标识。但需要注意,后续执行的加载是同步加载,可能阻塞任务执行(如单击任务,触发了延迟加载,那么运行时会去执行冷启动未加载的文件,从而增加耗时),因此是否使用lazy需要开发者自行评估。
25
26> **说明:**
27>
28> 不建议盲目增加lazy,这会增加编译和运行时的识别开销。
29
30## 场景行为解析
31
32- 使用lazy-import延迟加载。
33
34```typescript
35// main.ets
36import lazy { a } from "./mod1";    // "mod1" 未执行
37import { c } from "./mod2";         // "mod2" 执行
38
39// ...
40
41console.info("main executed");
42while (false) {
43    let xx = a;
44    let yy = c;
45}
46
47// mod1.ets
48export let a = "mod1 executed"
49console.info(a);
50
51// mod2.ets
52export let c = "mod2 executed"
53console.info(c);
54
55```
56
57执行结果为:
58
59```typescript
60mod2 executed
61main executed
62```
63
64- 同时对同一模块引用lazy-import与import。
65
66```typescript
67// main.ets
68import lazy { a } from "./mod1";    // "mod1" 未执行
69import { c } from "./mod2";         // "mod2" 执行
70import { b } from "./mod1";         // "mod1" 执行
71
72// ...
73
74console.info("main executed");
75while (false) {
76    let xx = a;
77    let yy = c;
78    let zz = b;
79}
80
81// mod1.ets
82export let a = "mod1 a executed"
83console.info(a);
84
85export let b = "mod1 b executed"
86console.info(b);
87
88// mod2.ets
89export let c = "mod2 c executed"
90console.info(c);
91
92```
93
94执行结果为:
95
96```typescript
97mod2 c executed
98mod1 a executed
99mod1 b executed
100main executed
101```
102
103如果在main.ets内删除lazy关键字,执行顺序如下:
104
105```typescript
106mod1 a executed
107mod1 b executed
108mod2 c executed
109main executed
110```
111
112## lazy-import与动态加载的区别
113
114lazy-import与[动态加载](./arkts-dynamic-import.md)都可以延后特定文件的执行时间,帮助设备分摊性能消耗,缓解特定时段的性能压力。
115
116| 区别项       | 动态加载                                         | lazy-import                                                 |
117|-----------|----------------------------------------------|-------------------------------------------------------------|
118| 语法示例      | let A = await import("./A");                 | import lazy { A } from "./A";                               |
119| 性能开销      | 1.创建异步任务开销。</br>2.执行到动态加载时,触发依赖模块的模块解析+源码执行。 | 1.lazy-import的模块解析在冷启动依旧会触发遍历。</br>2.导入的变量A被使用到时,触发模块的源码执行。 |
120| 使用位置      | 代码块/运行逻辑中使用                                  | 需要写在源码开头                                                    |
121| 是否可以运行时拼接 | 是                                            | 否                                                           |
122| 加载时序      | 异步                                           | 同步                                                          |
123
124lazy-import 相较于动态加载的优势:
125
1261. 在使用动态加载时,开发者需要将静态加载的代码(即同步导入)改写为动态加载语法(即异步导入),这可能涉及较大的代码修改量。
1272. 如果希望在冷启动阶段通过动态加载实现优化,开发者需要明确感知到被动态加载的文件在冷启动时不会被执行,否则会增加冷启动开销(放入异步队列等)。
1283. 相较于动态加载,使用 lazy-import 延迟加载,开发者只需在 import 语句中添加 lazy 关键字即可实现延迟加载,使用更加便捷。
129
130## 语法规格及起始支持版本
131
132- lazy-import支持如下指令实现:
133
134| 语法                                            | ModuleRequest  | ImportName | LocalName   | 开始支持的API版本 |
135|:----------------------------------------------|:---------------|:-----------|:------------|:-----------|
136| import lazy { x } from "mod";                 | "mod"          | "x"        | "x"         | API 12      |
137| import lazy { x as v } from "mod";            | "mod"          | "x"        | "v"         | API 12      |
138| import lazy x from "mod";                     | "mod"          | "default"  | "x"         | API 18      |
139| import lazy { KitClass } from "@kit.SomeKit"; | "@kit.SomeKit" | "KitClass" | "KitClass"  | API 18      |
140
141- 延迟加载共享模块或依赖路径内包含共享模块。
142    延迟加载对于共享模块依旧生效,使用限制参考[共享模块开发指导](../arkts-utils/arkts-sendable-module.md)。
143
144### 错误示例
145
146以下写法将引起编译报错。
147
148```typescript
149export lazy var v;                    // 编译器提示报错:应用编译报错
150export lazy default function f(){};   // 编译器提示报错:应用编译报错
151export lazy default function(){};     // 编译器提示报错:应用编译报错
152export lazy default 42;               // 编译器提示报错:应用编译报错
153export lazy { x };                    // 编译器提示报错:应用编译报错
154export lazy { x as v };               // 编译器提示报错:应用编译报错
155export lazy { x } from "mod";         // 编译器提示报错:应用编译报错
156export lazy { x as v } from "mod";    // 编译器提示报错:应用编译报错
157export lazy * from "mod";             // 编译器提示报错:应用编译报错
158
159import lazy * as ns from "mod";            // 编译器提示报错:应用编译报错
160import lazy KitClass from "@kit.SomeKit"   // 编译器提示报错:应用编译报错
161import lazy * as MyKit from "@kit.SomeKit" // 编译器提示报错:应用编译报错
162```
163
164与type关键词同时使用会导致编译报错。
165
166```typescript
167import lazy type { obj } from "./mod";    // 不支持,编译器、应用编译报错
168import type lazy { obj } from "./mod";    // 不支持,编译器、应用编译报错
169```
170
171### 不推荐用法
172
173- 在同一个ets文件中,期望延迟加载的依赖模块标记不完全。
174
175标记不完全将导致延迟加载失效,并且增加识别延迟加载的开销。
176
177```typescript
178// main.ets
179import lazy { a } from "./mod1";    // 从"mod1"内获取a对象,标记为延迟加载
180import { c } from "./mod2";
181import { b } from "./mod1";         // 再次获取"mod1"内属性,未标记lazy,"mod1"默认执行
182
183// ...
184```
185
186- 在同一ets文件中,未使用延迟加载变量并再次导出,不支持延迟加载变量被re-export导出,可以通过打开工程级build-profile.json5文件中的reExportCheckMode开关进行扫描排查。
187
188```typescript
189// build-profile.json5
190"arkOptions":{
191    "reExportCheckMode":"compatible"
192}
193```
194
195> **说明:**
196>
197> - 针对以下场景,编译时是否进行拦截报错:使用lazy import导入的变量,在同文件中被再次导出。
198> - noCheck(缺省默认值):不检查,不报错。
199> - compatible:兼容模式,报Warning。
200> - strict:严格模式,报Error。
201> - 该字段从DevEco Studio 5.0.13.200版本开始支持。
202
203这种方式导出的变量c未在B.ets中使用,因此B.ets不会触发执行。在A.ets中使用变量c时,由于该变量未被初始化,将会抛出JavaScript异常。
204
205```typescript
206// A.ets
207import { c } from "./B";
208console.info(c);
209
210// B.ets
211import lazy { c } from "./C";    // 从"C"内获取c对象,标记为延迟加载
212export { c }
213
214// C.ets
215let c = "c";
216export { c }
217```
218
219执行结果:
220
221```typescript
222ReferenceError: c is not initialized
223    at func_main_0 (A.ets:2:13)
224```
225
226```typescript
227// A_ns.ets
228import * as ns from "./B";
229console.info(ns.c);
230
231// B.ets
232import lazy { c } from "./C";    // 从“C”内获取c对象,标记为延迟加载
233export { c }
234
235// C.ets
236let c = "c";
237export { c }
238```
239
240执行结果:
241
242```typescript
243ReferenceError: module environment is undefined
244    at func_main_0 (A_ns.js:2:13)
245```
246
247### 注意事项
248
249- 不依赖该模块执行的副作用(如初始化全局变量,挂载globalThis等)。可参考:[模块加载副作用及优化](./arkts-module-side-effects.md)。
250- 使用导出对象时,触发延迟加载的耗时可能导致对应特性的功能劣化。由于lazy-import的后续加载是同步加载,可能在某些场景阻塞任务执行(比如在点击业务时触发了懒加载,那么运行时会执行冷启动为加载的文件,增加执行耗时,存在掉帧风险),是否使用延迟加载仍需要开发者自行评估。
251- 使用lazy特性可能导致模块未执行,从而引发bug。
252- 已经被动态加载的文件同时使用lazy-import时,这些文件会执行lazy标识,在动态加载的then逻辑中同步加载。
253
254## 可延迟加载文件检测
255
256本工具用于本地检测应用冷启动时的文件加载情况,可打印应用启动后固定时间段内使用和未使用的文件名,帮助开发者筛选可延迟加载的文件。
257
258> **说明:**
259>
260> 可延迟加载文件检测从API 20版本开始支持。
261
262### 检测步骤
263
2641. 打开工具:获取[hdc工具](../dfx/hdc.md#环境准备),连接设备,在终端直接输入下方命令执行。
265
266    ```shell
267    hdc shell param set persist.ark.properties 0x200105c
268    ```
269
2702. 可选项:设置抓取应用启动阶段的时间,单位为ms,范围为[100-30000],默认为2s。设置范围外的数字无法保证工具的计时准确性。
271
272    ```shell
273    hdc shell param set persist.ark.importDuration 1000
274    ```
275
2763. 清除应用后台进程后,重新启动应用进程,等待抓取时间结束,会在应用沙箱下(data/app/el2/100/base/${bundlename}/files/)生成主/子线程对应文件。
277
278    > **注意:**
279    >
280    > 1. 该工具仅支持本地安装的应用。
281    > 2. 生成文件的操作需要在当前进程存活时执行。
282    > 3. 如果抓取过程中进程退出,那么不会生成对应的文件。
283
2844. 关闭工具
285工具常开会损耗性能,使用后应及时关闭。
286
287    ```shell
288    hdc shell param set persist.ark.properties 0x000105c
289    ```
290
291### 生成文件介绍
292
293工具会根据设置的抓取时间,分别记录主线程和子线程在该时间内的文件加载情况。各线程独立计时。
294例如,设置时间为1秒,工具将记录主线程和子线程各自启动后1秒内的文件执行情况。
295
296文件生成路径:`data/app/el2/100/base/${bundleName}/files`
297主线程文件名:`${bundleName}_redundant_file.txt`
298子线程文件名:`${bundleName}_${tId}_redundant_file.txt`
299
300> **说明:**
301>
302> 1. 主线程文件名不含线程号信息,因此写入文件时会发生覆盖。
303> 2. 子线程文件名包含线程号tId,且每个tId唯一,确保每个子线程对应一个单独的文件。若需查找对应线程文件,可依据日志中的线程号或使用trace工具查看线程号进行匹配。
304
305**示例**
306当前测试应用bundleName为com.example.myapplication,应用内创建了一个子线程,线程号为18089(随机)。
307文件生成路径:data/app/el2/100/base/com.example.myapplication/files
308主线程文件名:data/app/el2/100/base/com.example.myapplication/files/com.example.myapplication_redundant_file.txt
309子线程文件名:data/app/el2/100/base/com.example.myapplication/files/com.example.myapplication_18089_redundant_file.txt
310![deferrable-tool-file](figures/deferrable-tool-file.png)
311
312### 检测原理
313
314如下例所示,A文件和B文件同时被Index文件依赖,那么A、B会随着Index文件的加载被直接加载执行。
315A文件执行过程完成了变量定义赋值并进行导出,对应A文件的耗时。B文件定义了一个函数并导出,对应B文件的耗时。
316在Index文件执行时,B文件的导出函数func被顶层执行,因此B文件的导出是无法优化的,在工具侧就会显示used。但是A文件的导出变量a在Index文件的myFunc函数被调用时才使用,如果冷启动阶段没有其他文件调用myFunc函数,那么A文件在工具侧就会显示unused,即可以延迟加载。
317
318 ```ts
319// Index.ets
320import { a } from './A';
321import { func } from './B';
322func(); // 使用B文件变量
323export function myFunc() {
324    return a; // a变量未被使用
325}
326// A.ets
327export let a = 10;
328
329// B.ets
330export function func() {
331    return 20;
332}
333```
334
335### 加载情况总结
336
337总结加载时间内所有文件及其耗时,包括已使用的文件及其耗时和未使用的文件及其耗时。
338例:
339
340```text
341<----Summary----> Total file number: 13, total time: 2ms, including used file:12, cost time: 1ms, and unused file: 1, cost time: 1ms
342```
343
344上述信息表示应用当前线程在冷启动抓取时间段内加载了13个文件,共耗时2ms。其中,12个文件导出内容被其他文件加载使用,执行这12个文件共耗时1ms;1个文件执行完成,但是其导出内容没有被其他文件在冷启阶段用到,耗时1ms。
345
346### 被使用文件
347
348在冷启动阶段,导出内容被其他文件使用的文件称为used file。
349
350- 场景1:通过静态加载加载的文件,其父文件(parentModule)代表该文件的引入方。
351
352    ```text
353    used file 1: &entry/src/main/ets/pages/1&, cost time: 0.248ms
354        parentModule 1: &entry/src/main/ets/pages/outter& a
355    ```
356
357    对应写法示例:
358
359    ```ts
360    // entry/src/main/ets/pages/outter.ets
361    import { a } from './1' // outter文件从1文件中加载了a变量
362    console.info("example ", a); // a变量在outter文件执行时就被使用
363    ```
364
365- 场景2:通过静态加载加载的文件,存在多个父文件。
366
367    ```text
368    // 说明:显示顺序不代表父文件的加载顺序。
369    used file 1: &entry/src/main/ets/pages/1&, cost time: 0.248ms
370       parentModule 1: &entry/src/main/ets/pages/outter& a
371       parentModule 2: &entry/src/main/ets/pages/innerinner& a
372    ```
373
374    对应写法示例:
375
376    ```ts
377    // entry/src/main/ets/pages/outter.ets
378    import { a } from './1' // outter文件从1文件中加载了a变量
379    console.info("example ", a); // a变量在outter文件执行时就被使用
380
381    // entry/src/main/ets/pages/innerinner.ets
382    import { a } from './1' // innerinner文件从1文件中加载了a变量
383    console.info("example ", a); // a变量在innerinner文件执行时就被使用
384    ```
385
386- 场景3:通过静态加载加载的文件,存在多个导出,但是只显示了一部分。
387
388    ```text
389    used file 1: &entry/src/main/ets/pages/1&, cost time: 0.248ms
390       parentModule 1: &entry/src/main/ets/pages/outter& a
391    ```
392
393    对应写法示例:
394
395    ```ts
396    // entry/src/main/ets/pages/outter.ets
397    import { a , b } from './1' // 加载1文件的多个变量
398    console.info("example ", a); // a被使用
399    export function myFunc() {
400     return b; // b未被使用
401    }
402    // entry/src/main/ets/pages/1.ets
403    export let a = 10;
404    export let b = 100;
405    ```
406
407- 场景4:动态加载或使用napi接口加载时,暂未支持父文件打印,因此不会显示父文件。
408
409    ```text
410    unused file 1: &entry/src/main/ets/pages/1&, cost time: 0.07ms
411    ```
412
413    对应写法示例:
414
415    ```ts
416    import("./1").then((ns:ESObject) => {
417        console.info('import file 1 success');
418    });
419    ```
420
421- 场景5:通过loadContent、pushUrl等接口加载的文件,其父文件(parentModule)统一显示为EntryPoint。
422
423    ```text
424    used file 1: &entry/src/main/ets/pages/Index&, cost time: 0.545ms
425    parentModule 1: EntryPoint
426    ```
427
428### 未被使用文件
429
430在冷启动阶段,导出内容没有被其他文件使用的文件称为未使用的文件,代表可以延迟加载。
431场景与被使用文件场景一致,但未被使用文件没有变量被使用的信息。
432
433- 场景:文件被这些父文件引用,但变量未被使用。可在引入未使用文件处(父文件)使用延迟加载方式加载该文件。
434
435    ```text
436    unused file 1: &entry/src/main/ets/pages/under1&, cost time: 0.001ms
437        parentModule 1: &entry/src/main/ets/pages/1&
438    ```
439
440    对应写法示例:
441
442    ```ts
443    // entry/src/main/ets/pages/1.ets
444    import { a } from './under1' // 加载under1文件的变量
445    export function myFunc() {
446     return a; // a未被使用
447    }
448    ```
449
450    可使用延迟加载:
451
452    ```ts
453    // entry/src/main/ets/pages/1.ets
454    import lazy { a } from './under1' // 不在此处触发under1文件的加载
455    export function myFunc() {
456     return a; // 此时触发under1文件的加载
457    }
458    ```
459
460### 使用示例
461
462**使用场景**
463
464下述例子中A文件被引用,在应用启动到点击按钮的这段时间里,A文件并没有被实际执行,在冷启动阶段加载A文件的行为属于冗余。
465
466```javascript
467// A为任意可以被引入的ets文件
468import { A } from "./A";
469
470@Entry
471@Component
472struct Index {
473  build() {
474    RelativeContainer() {
475      Button('点击执行A文件')
476        .onClick(() => {
477          // 点击后触发A文件的执行
478          console.log("执行A文件", A);
479        })
480    }
481    // ...
482  }
483}
484```
485
486![img](./figures/Lazy-Import-Instructions-1.png)
487
488通过抓取Trace图查看调用栈,可以发现应用在冷启动时加载了A文件。
489
490**使用工具分析**
491
4921. 连接设备,在终端直接输入下方命令执行。
493
494    ```shell
495    hdc shell param set persist.ark.properties 0x200105c
496    ```
497
4982. 启动应用,启动结束后关闭应用。
4993. 下载文件到本地,其中`${bundleName}`为应用名。
500
501    ```shell
502    hdc file recv data/app/el2/100/base/${bundleName}/files/${bundleName}_redundant_file.txt D:\
503    ```
504
5054. 对上述示例代码获取到的文件进行分析。
506
507   ![img](./figures/Lazy-Import-Instructions-2.png)
508
509**修改方式**
510
511工具筛选出冗余文件后,开发者可在引入时添加`lazy`关键字,标记文件可延迟加载。
512
513```javascript
514// 此处添加lazy关键字,标记该文件可延迟加载
515import lazy { A } from "./A";
516
517@Entry
518@Component
519struct Index {
520  build() {
521    RelativeContainer() {
522      Button('点击执行A文件')
523        .onClick(() => {
524          // 点击后触发A文件的执行
525          console.log("执行A文件", A);
526        })
527    }
528    // ...
529  }
530}
531```
532
533![img](./figures/Lazy-Import-Instructions-3.png)
534
535通过抓取Trace图查看调用栈可以发现,使用lazy-import标识后,应用在冷启动时不再加载A文件。
536
537**优化效果**
538
539|     | 加载文件耗时(微秒μs) |
540|-----|--------------|
541| 优化前 | 412us        |
542| 优化后 | 350us        |
543
544根据上述优化前后案例Trace图对比分析,使用延迟加载后应用冷启动时不再加载A文件,在资源加载阶段减少因加载冗余文件产生的耗时约15%,提高了应用冷启动性能。(由于案例仅演示场景,优化数据仅做参考,在实际业务中随着引用文件的复杂度提高,引用文件数量增多,优化效果也会随之提升。)
545