Flet 大列表渲染性能优化:ListView、GridView 与批量更新实战
【免费下载链接】fletBuild realtime web, mobile and desktop apps in Python only. No frontend experience required.项目地址: https://gitcode.com/gh_mirrors/fl/flet
导读
在 Flet 中展示包含数百甚至数千个条目的列表时,直接使用Column或Row会导致界面卡顿、滚动迟滞。本指南讲解 Flet 针对此类场景的核心方案:使用按需渲染的ListView与GridView控件,配合item_extent、runs_count、max_extent等性能参数,以及通过分批调用page.update()控制 WebSocket 消息体积的批量更新策略。读完本文,你将掌握在大数据量列表场景下写出流畅、响应迅速的 Flet 应用的完整实战方法。
问题:为什么 5000 条记录会让 Column/Row 卡顿
在大多数情况下,用Column和Row控件展示列表就足够了。但当列表包含数百甚至数千个条目时,Column和Row会变得低效、界面明显迟滞——因为它们会一次性渲染全部子控件,即使这些控件并不在当前滚动位置可见。
下面这个示例向页面添加了 5,000 个文本控件,而页面默认使用Column作为布局容器:
import flet as ft def main(page: ft.Page): for i in range(5000): page.controls.append(ft.Text(f"Line {i}")) page.scroll = "always" page.update() ft.run(main, view=ft.AppView.WEB_BROWSER)运行这段程序后你会发现:不仅首次加载并渲染所有文本行需要花上几秒钟,滚动时同样又慢又卡。原因在于:
- 5,000 个
Text控件全部被实例化并参与布局,即使其中绝大多数不可见; - 每次滚动都会触发大量控件的重建与绘制,超出 GPU 与 UI 线程的处理能力。
解决办法:展示大量条目的列表时,改用ListView和GridView控件——它们只在当前滚动位置可见的范围内按需渲染条目。
ListView:按需渲染的线性列表
ListView既可以是纵向(默认),也可以是横向(horizontal=True)的,条目沿滚动方向一个接一个排列。
ListView本身已经实现了高效的按需渲染,但滚动性能还可以进一步提升:如果能让所有条目拥有相同的高度(横向ListView则是相同的宽度),滚动引擎就能跳过逐项测量直接定位。Flet 提供两种方式:
| 方式 | 属性 | 说明 |
|---|---|---|
| 绝对尺寸 | item_extent | 为所有条目设定统一的固定高度(或宽度) |
| 首项原型 | first_item_prototype=True | 让所有条目的尺寸等同于第一个子控件的尺寸 |
实战:用 ListView 输出 5,000 个条目
import flet as ft def main(page: ft.Page): lv = ft.ListView(expand=True, spacing=10) for i in range(5000): lv.controls.append(ft.Text(f"Line {i}")) page.add(lv) ft.run(main, view=ft.AppView.WEB_BROWSER)换成ListView之后,滚动平滑且足够跟手,能实时响应鼠标移动。
关键细节:ListView 必须具有确定的高度(或宽度)
:::note 上例在ListView构造函数中使用了expand=True。ListView要正常工作,必须指定高度(横向时指定宽度)。你可以设置绝对尺寸,例如ListView(height=300, spacing=10),但上面的例子是让ListView占满页面所有可用空间,即 "expand"。更多内容参见Control.expand属性。 :::
源码级原理:builder 按需构建
在 Flet 的 Flutter 客户端实现 list_view.dart 中,build_controls_on_demand默认开启(True),此时控件会选用 Flutter 的ListView.builder或ListView.separated变体,仅在条目即将滚动进视口时才通过itemBuilder创建对应控件:
spacing > 0时使用ListView.separated,并在separatorBuilder中按divider_thickness决定插入Divider还是纯SizedBox间距(见 list_view.dart);spacing == 0时使用ListView.builder,并将itemExtent透传给 Flutter 引擎,让其在固定条目尺寸下跳过测量(见 list_view.dart)。
Python 侧对等定义见 list_view.py,其中明确了:
item_extent、prototype_item仅在build_controls_on_demand=True或spacing=0时生效;first_item_prototype=True时,客户端会把第一个子控件包装成prototypeItem参与ListView.builder的原型测量(见 list_view.dart);- 横向模式且外层高度无界(未设置
height、expand且未被父级约束)时,客户端会直接渲染错误提示"height is unbounded",要求开发者显式给出高度(见 list_view.dart)。
GridView:可滚动网格
GridView允许把控件排列进一个可滚动的网格中。
反例:用 Row(wrap=True) 模拟网格
你可以用ft.Column(wrap=True)或ft.Row(wrap=True)拼出"网格"效果,例如:
import os import flet as ft os.environ["FLET_WS_MAX_MESSAGE_SIZE"] = "8000000" def main(page: ft.Page): r = ft.Row(wrap=True, scroll="always", expand=True) page.add(r) for i in range(5000): r.controls.append( ft.Container( ft.Text(f"Item {i}"), width=100, height=100, alignment=ft.Alignment.CENTER, bgcolor=ft.Colors.AMBER_100, border=ft.Border.all(1, ft.Colors.AMBER_400), border_radius=ft.BorderRadius.all(5), ) ) page.update() ft.run(main, view=ft.AppView.WEB_BROWSER)尝试滚动和缩放浏览器窗口——一切都能工作,但非常卡顿。
:::note 程序开头设置了环境变量FLET_WS_MAX_MESSAGE_SIZE=8000000——这是 Flet Server 渲染页面时可接收的WebSocket 消息最大字节数。默认值为 1 MB,而描述 5,000 个Container控件的 JSON 消息会超过 1 MB,因此我们把允许值提升到 8 MB。
不过,通过 WebSocket 通道挤压大消息通常不是好主意,请改用下文批量更新的方式来控制通道负载。 :::
实战:用 GridView 重写上述示例
import os import flet as ft os.environ["FLET_WS_MAX_MESSAGE_SIZE"] = "8000000" def main(page: ft.Page): gv = ft.GridView(expand=True, max_extent=150, child_aspect_ratio=1) page.add(gv) for i in range(5000): gv.controls.append( ft.Container( ft.Text(f"Item {i}"), alignment=ft.Alignment.CENTER, bgcolor=ft.Colors.AMBER_100, border=ft.Border.all(1, ft.Colors.AMBER_400), border_radius=ft.BorderRadius.all(5), ) ) page.update() ft.run(main, view=ft.AppView.WEB_BROWSER)与ListView类似,GridView渲染大量子控件时非常高效。改用GridView后,滚动和窗口缩放都平滑且响应迅速。
网格布局参数:runs_count 与 max_extent
网格的"列数/行数(runs)"有两种指定方式:
runs_count:固定交叉轴方向(列或行)的子控件数量;max_extent:设置单个"格子"的最大尺寸,运行时列数/行数自动变化以适应容器宽度。
上例中我们把格子最大尺寸设为 150 像素,并用child_aspect_ratio=1将其形状设为"正方形"。child_aspect_ratio是每个子控件交叉轴与主轴尺寸的比值,可以尝试改成0.5或2观察效果差异。
源码级原理:两种网格代理
在 grid_view.dart 中可以看到,GridView客户端根据是否设置max_extent选择两种 Flutter 网格代理:
- 未设置
max_extent时使用SliverGridDelegateWithFixedCrossAxisCount,列数固定为runs_count; - 设置了
max_extent时改用SliverGridDelegateWithMaxCrossAxisExtent,列数由maxExtent与容器宽度自动推导。
默认build_controls_on_demand=True时走GridView.builder,条目按需构建(见 grid_view.dart);Python 侧默认值可查 grid_view.py:runs_count默认 1、max_extent默认None、spacing与run_spacing默认 10、child_aspect_ratio默认 1.0。其类注释也明确建议:处理数千条目的场景应优先选择GridView而非 wrap 的Column/Row,以获得平滑滚动。
批量更新:控制 WebSocket 消息负载
调用page.update()时,Flet 会通过 WebSocket 向 Flet Server 发送一条消息,内容包含自上次page.update()以来发生的页面更新。一次性发送包含数千个新增控件的超大消息,用户可能要等待几秒钟才能完整接收并渲染。
为了提升程序可用性、尽快把结果呈现给用户,可以分批发送页面更新。下面的程序把 5,100 个子控件按每批 500 个添加到ListView:
import flet as ft def main(page: ft.Page): # 先把 ListView 添加到页面 lv = ft.ListView(expand=1, spacing=10, item_extent=50) page.add(lv) for i in range(5100): lv.controls.append(ft.Text(f"Line {i}")) # 每 500 条发送一次更新 if i % 500 == 0: page.update() # 发送剩余的条目 page.update() ft.run(main, view=ft.AppView.WEB_BROWSER)注意这个例子同时设置了item_extent=50,配合每 500 条一次的page.update()分批推送,用户能很快看到列表头几百条,其余条目在后台持续到达并即时渲染。
批量更新的取舍建议
- 批次大小:批次越小,首屏响应越快,但
page.update()调用频率越高,WebSocket 往返次数也越多,需要根据实际条目复杂度权衡(本例 500 是一个常用起点); - 超大消息规避:
FLET_WS_MAX_MESSAGE_SIZE可以把允许的消息上限从默认 1 MB 调大,但这只是"扩容",并非根治——尽量通过批量更新把单条消息控制在合理体积内,而不是依赖调大该上限; - 渲染顺序:先把容器控件(
ListView/GridView)page.add()到页面并先行更新,再循环追加子控件并分批更新,保证用户能第一时间看到滚动容器本身。
总结与选择建议
| 场景 | 推荐方案 | 关键参数 |
|---|---|---|
| 数百至数千条线性列表 | ListView | item_extent/first_item_prototype,注意设置高度或expand |
| 数千条网格布局 | GridView | runs_count或max_extent+child_aspect_ratio |
| 一次性添加大量控件 | 分批调用page.update() | 每批 500 条左右,必要时调大FLET_WS_MAX_MESSAGE_SIZE |
核心结论:用ListView/GridView替代Column/Row承载大数据量列表,这是 Flet 官方 Cookbook(large-lists.md)给出的推荐做法,其底层通过 Flutter 的ListView.builder/GridView.builder按需构建实现(见 list_view.dart 与 grid_view.dart);再配合固定条目尺寸参数与批量更新策略,即可让包含数千条目的 Flet 应用保持流畅滚动与快速响应。
【免费下载链接】fletBuild realtime web, mobile and desktop apps in Python only. No frontend experience required.项目地址: https://gitcode.com/gh_mirrors/fl/flet
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考