Improving performance of inline editable tables

Dev Diary

Efficient data management often requires intuitive solutions, and inline editing in tables offers a practical way to make quick adjustments without leaving the context of the data. By allowing users to edit data directly in a table without navigating to a detail page or opening a popup, this method minimizes disruptions and speeds up workflows. However, implementing inline-editable tables comes with its challenges, especially when working with UI libraries like Angular Material, which do not natively support this feature. In this article, we explore the performance bottlenecks I encountered while integrating inline editing into an Angular Material table and discover different approaches on how to speed up initial loading times and performance of the tables.

Autorin
Michelle Weber
Datum
29. Januar 2025
Lesedauer
7 Minuten

The problem

We rely on Angular Material for the majority of our applications. Unfortunately, this UI library does not offer a table component providing inline editing - a feature we required for one of our projects. We wanted to keep the look and feel of the application streamlined. For that reason, we opted to use Material form fields inside the Material table component since the UI library's form fields were already used across the rest of the application. However, this approach caused long loading times in all tables since each table cell had to load and render a Material form field. This raised an important question: how can we enhance performance to deliver a fast and smooth user experience?

Possible approaches

Editing data in a detail page

Instead of allowing the user to edit the data inside the table we could just simply navigate him to a detail page and display the form fields there. The performance would be improved since not every table entry needs to render a form field for all of their editable properties. Unfortunately, this decreases the user experience a lot. By implementing a detail page, the user needs additional effort to get to where he wants to go. If the user had to edit several entries at once, it would quickly become a very time consuming and frustrating task. Therefore, this solution was not an option for our use case but could be the way to go for others.

Using HTML form fields

Instead of the Material components we could just use plain HTML form fields. This slightly improves the performance but decreases reusability. Why? Because we cannot wrap the plain HTML form fields inside an Angular component since this decreases the performance again. The only way to benefit from it is to use the form fields directly inside each inline-editable table. Using HTML form fields would also require us to replicate the form field’s styling from Angular Material to keep it aligned with the rest of the application. Besides styling, validation is also a concern and needs to be done in each page that displays a table. Of course, both can be achieved with shared CSS and functions, it is just not as convenient as putting everything together into a single component which then can be reused without having to take care of applying shared CSS and validation functions.

Using deferred loading

Deferred loading, which was introduced in Angular 17, allows loading content of a page on specific triggers. This not only reduces the initial bundle size but also speeds up initial load times which in turn means: performance improvements. In our particular case, we decided to go with the option of triggering the loading of a form field as soon as the user interacts with it. This way, no form fields are initially loaded, only the table with read-only data. This is much faster than any of the previously described approaches.

The solution explained

As described above, with deferred loading it is possible to define a trigger on which the form field is loaded. 

@defer (on hover; when focusedItemId === item.id) { <app-input-cell [value]="item.text" (focusChanged)="focusChanged(item.id)" (valueChanged)="updateValue(item.id, $event, 'text')" [required]="true" inputType="text" ></app-input-cell> } @placeholder { <div class="defer-list-placeholder" tabindex="0" type="input" > {{ item.text }} </div> }

As you can see in the code block above, we can use multiple triggers in a single @defer block. In this case, we trigger the loading of the input either on hovering the element or if one form field of the currently edited row is focused. The on hover trigger loads the deferred content either when the mouseover or the focusin event is fired. This way, we can ensure that the user can use the tab-key to go through all properties of the currently edited item and also the next item in the table. To make the placeholder div tabbable (and therefore making it possible to fire the focusin event when the user tabs to the next row in the table), we need to set the tabindex of it to 0. This is required since a div element is not focusable by default. As long as the user did not interact with a data row, the placeholders are displayed. These are styled to look like the real input fields, so the user is unaware that the content is being swapped.

Other possible defer triggers could be one of the following:

  • Loading as soon as the browser is idle (on idle)
  • When the user is hovering over the table (on hover(table))
  • Right after all non-deferrable content has been loaded (on immediate). 

I will describe in the next section why none of these approaches are feasible in our use case.

Performance comparison

To compare the performance we take one of the Core Web Vitals (CWV) measurements in consideration: INP (Interaction to Next Paint). More on CWV here.
Why INP? The three important measurements of CWV are Largest Contentful Paint (LCP), Interaction to Next Paint (INP) and Cumulative Layout Shift (CLS). Since LCP measures the initial loading time of the largest image, text block or video this measurement is not applicable for our testing objectives. CLS is a measurement to determine the shifts of elements on a web page. This also does not satisfy our testing purposes. INP assesses the overall responsiveness of a page. Since poor rendering performance decreases responsiveness, this measurement is perfect to elaborate on our performance. The INP measurement is considered to be good when it is below 200 ms, could be further improved between 200 and 500 ms and is considered to be poor if it is above 500 ms.

For the comparison we determine two different INPs:

  1. Navigation from an arbitrary page to the page containing the table to measure how long it takes until the data is displayed
  2. Directly loading the page containing the table and immediately clicking into a cell to edit a value

In the following table you can see the comparison of the initial approach with the different @defer approaches and the HTML form field approach. The values are averaged from 12 individual test runs. 

ApproachAverage INP navigationAverage INP edit value
Initial implementation without @defer2,664 ms47 ms
Currently used @defer approach353 ms154 ms
HTML form fields661 ms41 ms
@defer (on hover(table))281 ms2177 ms
@defer (on idle)334 ms2243 ms
@defer (on immediate)2702 ms48 ms

In the table above you can see the INP when navigating from a page to the table decreased by 2311 milliseconds, which is a performance improvement of 87%! It is still not in the “good” INP range and could of course be further improved, but compared to the INP of the initial approach it already performs a lot better. As you can see, the INP for editing a value is a bit worse but is still in the range to be considered as good. This value increased due to the fact that the form field needs to be loaded when being clicked which delays the interactability a tiny bit.
When it comes to HTML form fields, the navigation INP is also way better than before but still in the poor range. Although there is no Angular component that needs to be loaded, all the event listeners, etc. still need to be initialized simultaneously, which is the main reason for the overhead. 

Both @defer(on hover(table)) and @defer(on idle) have similar outcomes. The navigation INP is slightly better than in the currently used approach, but unfortunately the edit value INP is really poor. As described above, I assessed these values with the premise that the user wants to edit data right away. In these two cases, all of the form fields are loaded simultaneously when the user either hovers the table or when the browser is idle. This in turn causes long waiting times until all placeholders are replaced with the actual form field. These two approaches could be considered if we are sure that the user will not edit the data right away but rather stays on the page for some time before he starts doing so.

Finally, using @defer(on immediate) leads to basically the same outcome as the initial approach without using any deferrable content. This is because the form fields are loaded immediately after all other content is loaded, which in our particular use case makes no difference to the default loading behavior.

To balance the performance of navigating and interaction with the form fields we decided to go with the approach which loads each individual form field once the user interacts with it. This balances all important factors of user experience, performance and code reusability.

Conclusion

Optimizing performance of inline-editable tables is critical for delivering a smooth user experience. While alternative methods such as editing in detail pages or using plain HTML form fields can offer certain advantages, they often come at the cost of usability or maintainability. Deferred loading proved to be the most effective solution for our use case. We achieved significant performance improvements without compromising functionality or design consistency.

Mehr davon?

Email-Templating with Blazor_B
Dev Diary
Email-Templating with Blazor
31. Oktober 2024 | 7 Min.
Kubernetes Dashboard Login über OpenID-Connect_B
Dev Diary
Kubernetes Dashboard Login mit OpenID-Connect
12. September 2024 | 7 Min.

Kontaktformular

*Pflichtfeld
*Pflichtfeld
*Pflichtfeld
*Pflichtfeld

Wir schützen deine Daten

Wir bewahren deine persönlichen Daten sicher auf und geben sie nicht an Dritte weiter. Mehr dazu erfährst du in unseren Datenschutzbestimmungen.