Migrating from Sapper to SvelteKit

Meet Speedgrid - A high-performance Angular/HTML canvas component

by Oliver Küchen on October 25, 2021

I guess everyone who is at least a little bit involved with modern UX has heard it before: Datagrids are dinosaurs, there is no place for them anymore. Nobody wants to scroll through data, there are better ways to handle that.

Unfortunately, the reality is different: Almost all large projects still require datagrids and tables. The truth is, most of them are really hard to handle - especially in a browser. They stutter, they are slow and they load data frequently every time a user moves the mouse. Sometimes the reason is in the implementation of the component, other times in the backend.

There are many ways to implement a datagrid. Virtualization, for example, is a good start: Only necessary DOM nodes are rendered, recalculated and filled with different content on user input. In this way, you can actually process a large amount of data.

But is there an even faster way with more control over content? Certainly, because all those DOM accesses are still expensive and can be complicated to deal with.

What works for a game also works for other scenarios

The game industry is possibly the one branch of software development, where performance counts most. Nearly all decisions have an immediate impact on the frame rate and perception of the software. They can also dictate the hardware a gamer needs to play a game.

Therefore, the industry has developed many best practices and techniques over the years in expensive alpha and beta tests. Developers who have never got in touch with game development have probably never heard of most of them. For most projects, this may not be necessary. But why ignore all those learnings?

Any modern browser is able to draw a canvas, for 2D development or even in 3D with WebGL. In my opinion, the right application can improve the web experience by a lot.

So let’s move on to a real example I’d like to share with you in this post: A datagrid, powered by a little experience as game developer.

The datagrid usecase and how to build it

Datagrids or large data tables are a perfect fit for indexed calculated drawing without the burden of DOM nodes.

As we all know, fast backends are no magic. A simple .NET Core webservice or Java Spring service with a proper database in the background should be able to provide clients with thousands of datarows within a second. Given that the user doesn’t sit behind a 56k modem, of course. Modern javascript engines are also able to handle ten thousands of entites (well, even a lot more actually) easily. The bottleneck is usually the DOM.

So let’s just skip that and create a canvas, calculate how many rows we have, and draw them based on height and scroll index. No magic, simple but very effective!

Rendering body cells in a hypothetical example


// Assume columns are defined outside, as a configuration, and contain width per column.
const columns = [...]

// And assume we do not resize the grid for now, so we are working with constants
const maxVisibleRows = Math.ceil((gridHeight -
    (headerHeight + footerHeight)) / rowHeight) + 1;

// offsetX and offsetY are simply the scroll offsets
public renderBodyCells(offsetX: number, offsetY: number): void {
    const rowOffset = offsetY % rowHeight;
    let y = headerHeight - rowOffset;
    let posY = Math.ceil(offsetY / rowHeight) + (rowOffset ? 0 : 1 );

    for (let rowY = 0; rowY < this.maxVisibleRows; rowY ++) {
        let x = 0;

        for (let columnX = 0; columnX < columns.length; columnX ++) {
            if (column[columnX].width + x > offsetX) {

                // this body cell is visible, draw its background
                canvas.drawRect(x - offsetX, y, column[columnX].width, rowHeight);

                // We know the pixel positions and the datarow index (posY)
                // So draw other stuff here ...
            }

            x += column[columnX].width;
        }

        y += this.rowHeight;
        posY ++;
    }
}

Yes, thats it. A little bit of math and you have body cells, scrollable. Only what is visible is drawn, and all necessary information is available to render the entity and property contents into the cell. It’s just pseudo-code and i know it can be written in a more effective way, but this should be a simple example.

What about themes and styles?

One problem with the canvas element is that you cannot style it with CSS. Well, because there are no DOM nodes to select but just the canvas node itself. But that does not have to be a problem at all.

CSS variables can also be accessed in JavaScript and used within the canvas. Just define a theme interface for your component to give developers who use it access to the style.

The ngx-speedgrid Angular component

As you may have guessed from reading the title, you don’t have to do the basic work yourself. There is a component for that ;)

The ngx-speedgrid is a (documented) Angular component and uses my angular-canvas-base directive which handles events and encapsulates all the required canvas functions and properties. There are also some additional tools like creating a backbuffer to implement simple double buffering and more. Both are Open Source and available via NPM.

A live demo implementation says more than thousand words, so checkout these two grids with different themes, loading 10k data rows from a .NET Core Webservice that are coming from a mariaDB. This demo is not optimized for mobile (yet) but this is totally possible to do and I might do it later.

The current themes will be improved shortly, but I guess you’ll probably create your own, custom speedgrid theme to match your needs anyways. Still, i want to provide more and better themes within the package.

Demo Code

Here is a simple demonstration on how to use the speedgrid component:

<ngx-speedgrid
    style="width: 100%; height: 500px;"
    [columns]=" columns "
    [data]=" entities "
    (clicked)=" speedgridClicked($event) "
    (selectedCellsChanged)=" selectedCellsChanged($event) "
    (orderByChanged)=" orderByChanged($event) "
></ngx-speedgrid>
@Component({
    selector: 'app-root',
    templateUrl: './app.component.html',
    styleUrls: ['./app.component.scss']
})
export class AppComponent implements OnInit {
    public columns: SpeedgridColumn<YourEntity>[] = [
        {
            width: 100,
            property: 'numberField',
            bodyCellRenderer: new SpeedgridBodyCellRendererNumber(),
            label: 'Number'
        },
        {
            width: 250,
            property: 'id',
            label: 'Guid'
        },
        {
            width: 350,
            property: 'textField',
            label: 'Text'
        },
        {
            width: 100,
            property: 'dateField',
            bodyCellRenderer: new SpeedgridBodyCellRendererString(new DatePipe('en-US')),
            label: 'Date'
        },
        {
            width: 70,
            property: 'imageField',
            bodyCellRenderer: new SpeedgridBodyCellRendererImage(this.imageStorageService, 16, 16),
            label: 'Image'
        },
        {
            width: 50,
            property: 'booleanField',
            bodyCellRenderer: new SpeedgridBodyCellRendererBoolean(),
            label: 'Bool'
        }
    ];

    public entities: YourEntity[] = [];

    constructor(private imageStorageService: SpeedgridImageStorageService) {}

    public ngOnInit(): void {
        // get your data somewhere and put it into this.entities;
    }

    public speedgridClicked(location: SpeedgridLocation): void {
        // handle click
    }

    public selectedCellsChanged(cells: Readonly<SpeedgridLocation[]>): void {
        // handle selection change
    }

    public orderByChanged(pairs: Readonly<SpeedgridOrderByPair[]>): void {
        // handle order by request
    }
}

As you can see, the template is very simple. Just give the speedgrid a style so that it knows how to display itself.

If you are not happy with the options or want to switch to a different theme, add [options] and/or [theme] to the inputs and define those. You can find the available options in the documentation, and there is also an example for the theme. This is the default - simply extend it or implement your own based on the interface.

Content rendering is done by a CellRenderer that uses the theme to define fonts, spaces etc. and renders whatever you give it. Also note that most CellRenderers can use pipes to transform values, like the Date example shown here.

My Personal Opinion

I think canvas components can help increase the overall user experience in several ways. These components are faster and if you know what you are doing, even more is possible than with traditional CSS design-wise because you control every pixel of the canvas.

The speedgrid is a first step and maybe other components will follow from our side in the future. I know we did not invent this wheel, but it is a not really well-known one. Many developers seem to avoid the topic. However, there is no reason for that.

Lets put it more in the spotlight!

I will keep updating the speedgrid and implement more features in later versions. So keep an eye on it if you are interested in the topic.