news 2026/9/23 18:48:09

Flet 大列表渲染性能优化:ListView、GridView 与批量更新实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Flet 大列表渲染性能优化:ListView、GridView 与批量更新实战

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 中展示包含数百甚至数千个条目的列表时,直接使用ColumnRow会导致界面卡顿、滚动迟滞。本指南讲解 Flet 针对此类场景的核心方案:使用按需渲染的ListViewGridView控件,配合item_extentruns_countmax_extent等性能参数,以及通过分批调用page.update()控制 WebSocket 消息体积的批量更新策略。读完本文,你将掌握在大数据量列表场景下写出流畅、响应迅速的 Flet 应用的完整实战方法。

问题:为什么 5000 条记录会让 Column/Row 卡顿

在大多数情况下,用ColumnRow控件展示列表就足够了。但当列表包含数百甚至数千个条目时,ColumnRow会变得低效、界面明显迟滞——因为它们会一次性渲染全部子控件,即使这些控件并不在当前滚动位置可见。

下面这个示例向页面添加了 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 线程的处理能力。

解决办法:展示大量条目的列表时,改用ListViewGridView控件——它们只在当前滚动位置可见的范围内按需渲染条目

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=TrueListView要正常工作,必须指定高度(横向时指定宽度)。你可以设置绝对尺寸,例如ListView(height=300, spacing=10),但上面的例子是让ListView占满页面所有可用空间,即 "expand"。更多内容参见Control.expand属性。 :::

源码级原理:builder 按需构建

在 Flet 的 Flutter 客户端实现 list_view.dart 中,build_controls_on_demand默认开启(True),此时控件会选用 Flutter 的ListView.builderListView.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_extentprototype_item仅在build_controls_on_demand=Truespacing=0时生效;
  • first_item_prototype=True时,客户端会把第一个子控件包装成prototypeItem参与ListView.builder的原型测量(见 list_view.dart);
  • 横向模式且外层高度无界(未设置heightexpand且未被父级约束)时,客户端会直接渲染错误提示"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.52观察效果差异。

源码级原理:两种网格代理

在 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默认Nonespacingrun_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/GridViewpage.add()到页面并先行更新,再循环追加子控件并分批更新,保证用户能第一时间看到滚动容器本身。

总结与选择建议

场景推荐方案关键参数
数百至数千条线性列表ListViewitem_extent/first_item_prototype,注意设置高度或expand
数千条网格布局GridViewruns_countmax_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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/23 18:48:10

论文选题避坑指南:3个实战项目教你搞定性能优化

论文选题避坑指南:3个实战项目教你搞定性能优化 官方文档翻了三遍,核心逻辑还是云里雾里?别急,这是大多数开发者的通病。 MDN Web Docs 里的示例代码往往过于理想化,直接复制到你的工程里,性能直接崩盘。 今天不聊虚的,直接上 实战项目…

作者头像 李华
网站建设 2026/9/23 18:47:39

炉石传说冰冠堡垒攻略实战:面试必问的性能优化深水区

炉石传说冰冠堡垒攻略实战:面试必问的性能优化深水区 刚写完几行循环,程序跑不动?别慌,这是很多开发者从“会写代码”到“能扛项目”必须跨过的坎。 很多兄弟在面试中被问到“你的项目里做过哪些性能优化”,如果只会回答“加了索引”或者“用了缓存”,那基本就是陪跑。真正的 面试必问…

作者头像 李华
网站建设 2026/9/23 18:47:26

踩了无数坑才懂:MUSLE 速查手册,别再被 StackTrace 搞疯

踩了无数坑才懂:MUSLE 速查手册,别再被 StackTrace 搞疯 盯着屏幕上一长串红色的 StackTrace,你肯定在想:这玩意儿到底哪行代码炸了?别慌,我见过太多后端工程师在凌晨三点对着 MUSLE 报错抓狂。这不仅是代码问题,更是架构认知偏差。今天这份 MUSLE…

作者头像 李华
网站建设 2026/9/23 18:47:26

DNF每日签到脚本翻车实录:新手避坑指南与底层逻辑拆解

DNF每日签到脚本翻车实录:新手避坑指南与底层逻辑拆解 配置环境就卡半天?别急,这不是你的错。 很多新手一上来就想着写个脚本自动刷DNF每日签到,结果代码跑不起来,报错满天飞,甚至账号直接被封。这背后的坑,比你想象的要深得多。今天咱们不整虚的,直接拆解这个【dnf每日签到】自动化的底层逻辑,告诉你为…

作者头像 李华
网站建设 2026/9/23 18:47:11

3步搞定0755tt图解原理:从语法到实战避坑指南

3步搞定0755tt图解原理:从语法到实战避坑指南 刚把Python基础语法背得滚瓜烂熟,一上手搭项目就卡壳?别慌,这正是90%新手的通病。学会语法却不知怎么搭项目,往往是因为你只记住了 print("Hello") ,却没搞懂数据在内存里是怎么流动的。今天这篇 0755tt…

作者头像 李华