Cory Rylan

My name is , Google Developer Expert, Speaker, Software Developer. Building Design Systems and Web Components.

Follow @coryrylan
Lit Web Components

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.

HTML Table with no CSS Contain

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.

HTML Table with CSS Contain

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.

HTML Table with 40,000 buttons

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.

HTML Table with 40,000 buttons with CSS Contain

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!

View Demo Code   
Twitter Facebook LinkedIn Email
 

No spam. Short occasional updates on Web Development articles, videos, and new courses in your inbox.

Related Posts

Lit Web Components

High Performance HTML Tables with Lit and Virtual Scrolling

Learn how to easily create HTML tables in Lit from dynamic data sources.

Read Article
Lit Web Components

Creating Dynamic Tables in Lit

Learn how to easily create HTML tables in Lit from dynamic data sources.

Read Article
RxJS

Using RxJS in Lit Web Components

Learn how to leverage RxJS in Lit based Web Components for reactive data streams.

Read Article