High Performance HTML Tables with Lit and CSS Contain
Cory Rylan
- 4 minutes
Web applications often need to handle large amounts of data, with HTML tables being a common method for presentation. However, traditional HTML tables can become slow and unresponsive when dealing with thousands of rows. Fortunately, with Lit, a modern, lightweight web component library, and the magic of CSS contain, you can create high performance tables with ease.
Introduction
When working with large data sets that require display in an HTML table, performance can become a bottleneck. By combining Lit with the CSS contain
property, we can achieve a high-performance table rendering experience.
Lit
The example uses TypeScript and Lit for defining a custom element. Lit is a lightweight web component library that provides a powerful templating engine for creating custom elements. While this example uses lit to render our HTML, we will be focusing on the render performance of the table and CSS itself, not the JavaScript execution time.
Below is our Lit custom element definition. This component will create a table with 10,000 rows and 4 columns. We will keep the content and CSS to a minimum to focus on the performance of the table itself.
import { html, css, LitElement } from 'lit';
import { customElement } from 'lit/decorators/custom-element.js';
@customElement('ui-element')
export class Element extends LitElement {
static styles = [css`...`];
#items = Array.from(Array(10000).keys()).map(i => ({ id: i, status: 'status', task: 'task', value: 'value' }));
render() {
return html`
<table>
<thead>
<tr>
${Object.keys(this.#items[0]).map(i => html`<th>${i}</th>`)}
</tr>
</thead>
<tbody>
${this.#items.map(i => html`
<tr>
${Object.keys(i).map(key => html`<td>${(i as any)[key]}</td>`)}
</tr>`)}
</tbody>
</table>
`;
}
}
The table is populated with dynamic data stored in the #items
array.
#items = Array.from(Array(10000).keys()).map(i => ({ id: i, status: 'status', task: 'task', value: 'value' }));
The render
method makes use of Lit's html
tag to create the table dynamically.
render() {
return html`
<table>
<!-- header and body -->
</table>
`;
}
Let's take a look at the performance profile of this table with a bound scroll height of 300px.
Render Performance
Below is our HTML Table rendered with a Lit based Web Component. This table is rendering 10,000 rows and 4000 cells.
Our render time on a Macbook M1 Pro hits around ~950ms. We can see the majority of the work is in the render phase (purple) and only a fraction is JavaScript execution (yellow). Even with only a fraction of the rows visible in the viewport, the browser still needs to render all 10,000 rows.
CSS Contain
The static styles
property includes all CSS needed for the table and the host element. Here, the contain
property improves performance by isolating the rendering of the element.
:host {
contain: strict;
contain-intrinsic-height: 300px;
}
The CSS Containment: Isolates the element from the rest of the layout, reducing layout and paint times. The Intrinsic Heights: contain-intrinsic-height
allows browsers to lay out the page more efficiently by providing a expected default height to prevent layout reflow calculations
The table row adds similar properties with the addition of content-visibility: auto
, which skips rendering of off-screen elements. This is property is what gives a huge performance boost to the table.
& tr {
contain: strict;
content-visibility: auto;
contain-intrinsic-height: auto 42px;
}
Now let's take a look at the same table withour CSS contain adjustments.
Our render time on a Macbook M1 Pro hits around ~500ms. We can see the render has dropped quite a bit. A large amount of the work is no longer required because the browser is no longer rendering all 10,000 rows, but only the rows visible in the viewport.
Now sub second renders with both versions of the table may be negligible, but the performance gains will be more noticeable on more complex tables or lower end devices. Let's update the grid to contain a button in each cell to introduce some complexity but omit the CSS Contain properties.
We can see the render time jumps significantly, on a Macbook M1 pro the render time is ~11 seconds. This is because the browser is rendering all 40,000 buttons, even though only a fraction are visible in the viewport. Let's add back the CSS Contain properties.
Our render time drops down to about ~500ms. This is a huge performance gain, especially when dealing with complex tables. It is not ideal to ever have tables if this size from an accessibility and user experience standpoint, but it is a good example of how CSS contain can improve performance.
Conclusion
Using CSS contain property can yield a highly performant rendering, capable of handling large datasets without sacrificing user experience. Checkout the working demo below!