双栏页面进入收尾阶段时,验收表上最常见的一句话是“功能都能点”。这句话只能说明路径可达,离长期可用还差一段距离。用户用平板快速滑动几十条消息,选中一条后立即返回列表,又将窗口收窄到单栏;如果列表每次都整批重建,详情切换夹杂同步解析,界面便会在高频操作中暴露出性能问题。比单次打开快不快更值得关注的,是同一条操作链上每一环能否保持连贯。

前五篇已经确定双栏导航、返回路径、共享选中态、可拖动宽度和阅读状态恢复的职责。本篇把这些机制放到同一张发布检查清单中:以真实交互过程为单位识别耗时,将高频列表更新与详情加载隔离,再检查窗口切换、回退和数据恢复是否破坏用户的阅读位置。

HarmonyOS 大屏主从布局的性能治理与发布复核

把性能问题还原成可追踪的操作链

“滑动不顺”不是一个适合直接修复的技术问题。需要把用户操作拆成连续事件:列表开始滑动、停住、点开详情、详情开始获取数据、返回、再次选中、调整分栏宽度。每个事件都对应明确的触发者,才有机会判断瓶颈在状态刷新、组件布局、图片解码,还是业务处理。

在消息阅读工作台中,列表项只展示标题、摘要、时间和已读标记;正文和附件属于详情区。滚动列表不应驱动详情正文反复装载,详情刷新也不应让所有列表项重新构建。页面可以让选中 ID 保持在统一状态源中,同时将完整正文、附件和富文本解析放在异步业务层。共享的是必要标识,不是全部重量级对象。

用户操作需要关注的变化不应连带发生的行为
快速滑动列表可见列表项位置与少量预取项批量构建所有正文组件
点开另一条消息选中 ID、详情请求及高亮刷新不相关列表行的数据
返回列表详情导航栈与当前高亮清空已加载的消息索引
调整分栏宽度左右栏尺寸及文本排版重置消息选中项和滚动锚点
前后台切换会话快照与必要资源重复保存整棵组件结构

用懒加载约束列表的节点数量

ArkUI List 搭配 LazyForEach,可以按照可见区域构建列表项;而 List 搭配普通 ForEach 时会一次性创建所有子组件。对于条目持续增加的阅读工作台,懒加载应成为列表结构设计的一部分。LazyForEach 依赖 IDataSource 提供数据和变更通知,删除、替换与插入时必须保持监听器和数据一致,不能只修改普通数组而忽略 UI 更新。

// message/MessageSource.ets:一个可独立复用的列表数据源
export interface MessageRow {
  id: string;
  title: string;
  summary: string;
  read: boolean;
}

export class MessageSource implements IDataSource {
  private rows: MessageRow[];
  private listeners: DataChangeListener[] = [];

  constructor(rows: MessageRow[]) { this.rows = rows; }
  totalCount(): number { return this.rows.length; }
  getData(index: number): MessageRow { return this.rows[index]; }

  registerDataChangeListener(listener: DataChangeListener): void {
    if (this.listeners.indexOf(listener) < 0) this.listeners.push(listener);
  }
  unregisterDataChangeListener(listener: DataChangeListener): void {
    const index: number = this.listeners.indexOf(listener);
    if (index >= 0) this.listeners.splice(index, 1);
  }
  replace(index: number, row: MessageRow): void {
    if (index < 0 || index >= this.rows.length) return;
    this.rows[index] = row;
    this.listeners.forEach((listener: DataChangeListener) => listener.onDataChange(index));
  }
}

这段实现刻意只提供单行替换。一次只改已读标记时,知道具体索引比直接宣布全量重载更有利于控制刷新范围。真实业务还应为插入、删除、排序设计相应的通知,并将来自网络的消息按 ID 合并;排序导致索引变化时,要按最终数据顺序通知,避免数据位置与高亮错位。

列表项的 key 也必须稳定。若使用数组索引充当标识,插入一条置顶消息后,旧索引代表的内容会改变,容易引出子组件状态错配。选择消息自身不会变化的业务 ID,更符合消息工作台的更新特征。

// pages/InboxPane.ets:列表部分,正文独立交给详情区
import { MessageSource, MessageRow } from '../message/MessageSource';

@Component
export struct InboxPane {
  private source: MessageSource = new MessageSource([
    { id: 'a01', title: '版本评审', summary: '评审意见已汇总', read: false },
    { id: 'a02', title: '交付清单', summary: '请复核待办项', read: true }
  ]);
  @Prop selectedId: string = '';
  onSelect: (id: string) => void = () => {};

  build() {
    List({ space: 4 }) {
      LazyForEach(this.source, (row: MessageRow) => {
        ListItem() {
          Column({ space: 6 }) {
            Text(row.title).fontSize(17).fontWeight(FontWeight.Medium)
            Text(row.summary).fontSize(13).fontColor('#667085')
          }
          .padding(14).width('100%')
          .backgroundColor(this.selectedId === row.id ? '#E8F2FF' : '#FFFFFF')
        }.onClick(() => this.onSelect(row.id))
      }, (row: MessageRow) => row.id)
    }
    .cachedCount(3)
    .width('100%')
    .height('100%')
  }
}

这里的 cachedCount(3) 是预取范围的初始配置值,不是对所有设备都成立的最佳数值。缓存数提高通常会增加预创建成本和内存使用,实际取舍应根据行高、图像比例和滚动特征确定。代码示例还需要在使用处从数据源模块导入 MessageSource 和 MessageRow,并由父级维护 selectedId,让导航切换不改变消息身份。

DevEco Studio 中列表懒加载与性能定位的技术布局

不要在渲染路径里做同步重活

如果详情包含 Markdown 解析、附件信息整理、复杂排序或较大的 JSON 数据转换,就需要把它们与组件 build() 分离。build() 应根据已有状态描述界面,而不是在构建期间继续修改状态并触发新的重建。数据请求也不应由每一次状态刷新重复启动;把获取动作放到明确的用户行为或页面生命周期阶段,再用请求序号排除过期响应,能同时减少重复任务与闪烁。

诊断时,建议围绕一段固定的用户路径采样:连续滑动、点击列表项、切换详情、返回并切换窗口宽度。DevEco Profiler 能从 Frame 泳道观察卡顿区间,再结合业务操作和 ArkUI 调用栈寻找可疑工作。不能仅凭某一次主观感受判断已经优化成功;更有价值的是保存相同输入条件下的前后比较,包括页面结构、数据量、图片资源和设备刷新率。

图片资源往往容易被忽略:列表缩略图不需要下载或解码成详情大图的尺寸。行组件避免不必要的阴影、模糊和深层容器嵌套;当更新只涉及已读标志时,尽量将局部可变数据落在局部组件中,而不要把整份富文本对象塞进高频列表项。系统级导航和业务级数据仓库各自负责自己的部分,性能问题才不会互相放大。

把发布前复核写成可执行的检查场景

相同代码在不同窗口尺寸下,会出现不同的呈现路径。团队可以把检查对象划分为三个矩阵,而不是用一台设备的一次体验代替整体结论。

复核维度操作组合重点观察
布局宽屏双栏、窄窗单栏、分隔线拖拽是否有裁剪、遮挡和不可点击区域
导航列表→详情→深层内容→返回路由栈、选中高亮和焦点是否一致
数据刷新列表、收到新增项、切换已读数据 ID 和显示位置是否对应
生命周期后台返回、窗口重建、阅读恢复选中项及文章锚点是否复原
性能长列表滚动、重复打开详情、缩放窗口帧耗时、内存变化及重复任务
无障碍放大字体、键盘焦点、屏幕阅读信息顺序与可操作控件是否完整

实际组织复核时,特别要把“在加载详情期间切换另一个消息”和“拖动分栏时恰好有列表更新”列入场景。这两条路径能同时检验异步数据序号、选中态、节点复用和窗口适配。如果业务逻辑只是把结果直接写进当前组件,过期请求容易覆盖新详情;如果业务层使用 ID 匹配和请求序号,界面更容易保持可解释的状态。

可以用一段轻量的纯函数为发布检查建立确定的判定基础,例如把窗口可用宽度、左栏最小宽度和右栏最小宽度分离计算。它并不替代 Navigation 自身的测量布局,而是让工程侧对于最小显示条件拥有清晰的输入输出。

export interface PaneDecision {
  useSplit: boolean;
  leftMin: number;
  rightMin: number;
}

export function decidePane(widthVp: number): PaneDecision {
  const leftMin: number = 260;
  const rightMin: number = 360;
  const divider: number = 1;
  return {
    useSplit: widthVp >= leftMin + rightMin + divider,
    leftMin: leftMin,
    rightMin: rightMin
  };
}

// 发布复核用例数据:可用于单元测试进行边界断言
const cases: number[] = [600, 620, 621, 720, 960];
cases.forEach((w: number) => {
  const decision: PaneDecision = decidePane(w);
  console.info(`width=${w}, split=${decision.useSplit}`);
});

边界值 621vp 来自本例的业务约束,不是平台统一规定的平行视界分栏阈值。一个工程不应把“平板”与“必然双栏”等同起来,真正影响布局的是应用当时可用的内容区域。用户把平板窗口缩窄时,单栏的操作和导航也应保持完整。

手机窄窗场景的列表浏览与发布前检查界面

收官检查应留下一份可信的工程依据

这一套连载建立的工程边界是明确的:Navigation 管页面容器与回退;业务仓库管消息身份、阅读状态及版本合并;布局策略根据窗口空间决定单栏或双栏;持久化保存业务上有意义的锚点;列表使用稳定键和按需构建控制更新范围。发布前复核把这些机制连起来,目的不是追求某个好看的性能数字,而是避免一个地方的优化破坏其他地方的连续性。

最终验收可将操作步骤、输入消息集合、窗口宽度和发现的问题对应起来,由开发团队在可复现条件下逐项确认。对于尚未达到预期的场景,保留追踪点并明确修复位置;对于已满足要求的场景,仍需在依赖升级或布局改动之后重跑相关路径。大屏体验真正经得起使用的标志,是用户不需要意识到页面栈、数据源、缓存和窗口生命周期正在协作。

参考文档

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐