New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Table is slow/laggy when there are a lot of items #1004
Comments
I have the same issue with my tree widget and a lot of data. Especially scrolling was really sluggish. I delayed that problem with debounced input events. Only render every 20 ms or so and not on every event. But that does not solve the issue of a single render being slow. My current leading thought is an abstraction of text and height. In order to know which items are shown with the given offset every height of every item is needed which results in doing text().height() for many items. // oversimplified
trait TreeItem {
fn to_text(&self) -> Text<'_>;
fn height(&self) -> usize {
self.to_text().height()
}
} With this trait the implementation could use the default height method which uses the text or override it for performance benefits. As an alternative idea the text / height could be cached once and only the cache be used then. I have nothing of that benchmarked. I am just throwing ideas around. |
Some findings from Discord: Generating the rows once makes it slightly faster: diff --git a/examples/table.rs b/examples/table.rs
index 744e142..a372ac4 100644
--- a/examples/table.rs
+++ b/examples/table.rs
@@ -88,18 +88,36 @@ impl Data {
}
}
-struct App {
+struct App<'a> {
state: TableState,
items: Vec<Data>,
longest_item_lens: (u16, u16, u16), // order is (name, address, email)
scroll_state: ScrollbarState,
colors: TableColors,
color_index: usize,
+ rows: Vec<Row<'a>>,
}
-impl App {
+impl<'a> App<'a> {
fn new() -> Self {
let data_vec = generate_fake_names();
+ let colors = TableColors::new(&PALETTES[0]);
+ let rows = data_vec
+ .iter()
+ .enumerate()
+ .map(|(i, data)| {
+ let color = match i % 2 {
+ 0 => colors.normal_row_color,
+ _ => colors.alt_row_color,
+ };
+ let item = data.ref_array();
+ item.into_iter()
+ .map(|content| Cell::from(Text::from(format!("\n{content}\n"))))
+ .collect::<Row>()
+ .style(Style::new().fg(colors.row_fg).bg(color))
+ .height(4)
+ })
+ .collect();
Self {
state: TableState::default().with_selected(0),
longest_item_lens: constraint_len_calculator(&data_vec),
@@ -107,6 +125,7 @@ impl App {
colors: TableColors::new(&PALETTES[0]),
color_index: 0,
items: data_vec,
+ rows,
}
}
pub fn next(&mut self) {
@@ -156,7 +175,7 @@ impl App {
fn generate_fake_names() -> Vec<Data> {
use fakeit::{address, contact, name};
- (0..20)
+ (0..15000)
.map(|_| {
let name = name::full();
let address = format!(
@@ -252,21 +271,9 @@ fn render_table(f: &mut Frame, app: &mut App, area: Rect) {
.collect::<Row>()
.style(header_style)
.height(1);
- let rows = app.items.iter().enumerate().map(|(i, data)| {
- let color = match i % 2 {
- 0 => app.colors.normal_row_color,
- _ => app.colors.alt_row_color,
- };
- let item = data.ref_array();
- item.into_iter()
- .map(|content| Cell::from(Text::from(format!("\n{content}\n"))))
- .collect::<Row>()
- .style(Style::new().fg(app.colors.row_fg).bg(color))
- .height(4)
- });
let bar = " █ ";
let t = Table::new(
- rows,
+ app.rows.clone(),
[
// + 1 is for padding.
Constraint::Length(app.longest_item_lens.0 + 1), Or we can simply store diff --git a/examples/table.rs b/examples/table.rs
index 744e142..aba0e31 100644
--- a/examples/table.rs
+++ b/examples/table.rs
@@ -88,25 +88,74 @@ impl Data {
}
}
-struct App {
+struct App<'a> {
state: TableState,
items: Vec<Data>,
- longest_item_lens: (u16, u16, u16), // order is (name, address, email)
scroll_state: ScrollbarState,
colors: TableColors,
color_index: usize,
+ table: Table<'a>,
}
-impl App {
+impl<'a> App<'a> {
fn new() -> Self {
let data_vec = generate_fake_names();
+ let colors = TableColors::new(&PALETTES[0]);
+ let rows: Vec<Row> = data_vec
+ .iter()
+ .enumerate()
+ .map(|(i, data)| {
+ let color = match i % 2 {
+ 0 => colors.normal_row_color,
+ _ => colors.alt_row_color,
+ };
+ let item = data.ref_array();
+ item.into_iter()
+ .map(|content| Cell::from(Text::from(format!("\n{content}\n"))))
+ .collect::<Row>()
+ .style(Style::new().fg(colors.row_fg).bg(color))
+ .height(4)
+ })
+ .collect();
+ let header_style = Style::default().fg(colors.header_fg).bg(colors.header_bg);
+ let selected_style = Style::default()
+ .add_modifier(Modifier::REVERSED)
+ .fg(colors.selected_style_fg);
+
+ let header = ["Name", "Address", "Email"]
+ .into_iter()
+ .map(Cell::from)
+ .collect::<Row>()
+ .style(header_style)
+ .height(1);
+ let bar = " █ ";
+ let longest_item_lens = constraint_len_calculator(&data_vec);
+ let table = Table::new(
+ rows.clone(),
+ [
+ // + 1 is for padding.
+ Constraint::Length(longest_item_lens.0 + 1),
+ Constraint::Min(longest_item_lens.1 + 1),
+ Constraint::Min(longest_item_lens.2),
+ ],
+ )
+ .header(header)
+ .highlight_style(selected_style)
+ .highlight_symbol(Text::from(vec![
+ "".into(),
+ bar.into(),
+ bar.into(),
+ "".into(),
+ ]))
+ .bg(colors.buffer_bg)
+ .highlight_spacing(HighlightSpacing::Always);
Self {
state: TableState::default().with_selected(0),
- longest_item_lens: constraint_len_calculator(&data_vec),
scroll_state: ScrollbarState::new((data_vec.len() - 1) * ITEM_HEIGHT),
colors: TableColors::new(&PALETTES[0]),
color_index: 0,
items: data_vec,
+ table,
}
}
pub fn next(&mut self) {
@@ -156,7 +205,7 @@ impl App {
fn generate_fake_names() -> Vec<Data> {
use fakeit::{address, contact, name};
- (0..20)
+ (0..15000)
.map(|_| {
let name = name::full();
let address = format!(
@@ -239,52 +288,7 @@ fn ui(f: &mut Frame, app: &mut App) {
}
fn render_table(f: &mut Frame, app: &mut App, area: Rect) {
- let header_style = Style::default()
- .fg(app.colors.header_fg)
- .bg(app.colors.header_bg);
- let selected_style = Style::default()
- .add_modifier(Modifier::REVERSED)
- .fg(app.colors.selected_style_fg);
-
- let header = ["Name", "Address", "Email"]
- .into_iter()
- .map(Cell::from)
- .collect::<Row>()
- .style(header_style)
- .height(1);
- let rows = app.items.iter().enumerate().map(|(i, data)| {
- let color = match i % 2 {
- 0 => app.colors.normal_row_color,
- _ => app.colors.alt_row_color,
- };
- let item = data.ref_array();
- item.into_iter()
- .map(|content| Cell::from(Text::from(format!("\n{content}\n"))))
- .collect::<Row>()
- .style(Style::new().fg(app.colors.row_fg).bg(color))
- .height(4)
- });
- let bar = " █ ";
- let t = Table::new(
- rows,
- [
- // + 1 is for padding.
- Constraint::Length(app.longest_item_lens.0 + 1),
- Constraint::Min(app.longest_item_lens.1 + 1),
- Constraint::Min(app.longest_item_lens.2),
- ],
- )
- .header(header)
- .highlight_style(selected_style)
- .highlight_symbol(Text::from(vec![
- "".into(),
- bar.into(),
- bar.into(),
- "".into(),
- ]))
- .bg(app.colors.buffer_bg)
- .highlight_spacing(HighlightSpacing::Always);
- f.render_stateful_widget(t, area, &mut app.state);
+ f.render_stateful_widget(&app.table, area, &mut app.state);
}
fn constraint_len_calculator(items: &[Data]) -> (u16, u16, u16) { But still, I don't think these solutions aren't optimal. I will look into the height issue that @EdJoPaTo pointed out to see if it has any effect on the performance. |
In case you're interested, in my library I've solved this problem by adding a trait that provides the widget size pre-render. When implementing this trait and Widget on the reference instead of the value of the list element, it allows for lazy evaluation and I can basically render any number of elements, here is an example |
One possible approach for improving the table performance in cases where there are a lot of rows is to get only the rows that need to be rendered on demand. I tried to write a PoC that demonstrates that where the main struggle was to not break the |
As a pretty immediate workaround, you can kinda do this outside of the table pretty easily also:
This is an easy brute force approach to fixing this (i.e. we just throw away a bunch of unnecessary data without worrying too much about getting the amount exactly right as the order of magnitude amount discarded is the thing that matters, not the exact amount). |
That makes sense. I already implemented a brute force approach here: orhun/binsider@e86b10c Basically I do some skipping/limiting of items to only render a certain part of the table. |
My Interestingly this approach also has improved benchmarks for small data which mostly fits into the buffer. An empty tree takes 5% less time while a relatively small unstyled EdJoPaTo/tui-rs-tree-widget#33 This is likely to be especially effective as large sections of tree data is closed by default. But other widgets like tables should benefit from this approach too as there are not that many items that can be rendered into the view anyway. |
Description
In one of my TUI projects I have 15k items in a
Table
and the render is pretty slow and laggy. I press the key to scroll the table but it takes around 1-2 seconds.To Reproduce
To confirm it, I tried this with the table example as well.
Just apply this patch:
Expected behavior
Normal table performance.
Screenshots
Provided above.
Environment
Additional context
--release
make things a bit faster but it is still laggy.The text was updated successfully, but these errors were encountered: