TOAST UI Grid is a UI library that enables users to handle complex tabular data on the web, and a major update of version 4 has been released last week! In version 4, we got rid of the old Backbone and jQuery, and rewrote the entire codebase again using Preact. Preact is a tiny but powerful library of 8KB when minified (3KB when compressed using Gzip,) that provides API that is similar to React as well as a virtual DOM.
The purpose of this article is to explain why we decided to use Preact for the new version of the Grid, and what we got out of using Preact. For those who do not want to study the article, I have boiled down the main ideas of this article into three points.
TL;DR
- The virtual DOM enables you to write concise and declarative UI codes. Rewriting the original codebase of Backbone and jQuery using Preact, we were able to decrease the bundle size from 327KB to 154KB.
- The virtual DOM is in no way slow. Virtual scroll, especially, allows you to implement a complex UI that handles millions of data sets without performance drops.
- Preact provides a very powerful virtual DOM in a very lean package. Preact can be a great choice if you are writing a UI library that is independent of any particular frameworks.
Do I have your attention now? Let’s take a deep breathe and dive in!
What is a Virtual DOM?
Before I introduce Preact, I think a preliminary introduction of the virtual DOM is necessary. The virtual DOM is a tree structure that is identical to the actual DOM tree and is implemented using the JavaScript’s object. Although React was not the first to implement this idea, it was mostly made popular by React, and now numerous frameworks like Vue.js, Cycle.js, Mithril.js, and Marko are built on the idea of virtual DOM.
Since most of the virtual DOM is implemented using plain objects, it is extremely cheap to implement, and a virtual DOM is mainly used not to directly manipulate the structure but simply to represent a data structure. Applications that use a virtual DOM reconstruct the virtual DOM tree every time the state changes inside of the application, and use the difference between the older virtual DOM tree and the newly constructed virtual DOM tree to apply the changes to the actual DOM.
While this method is faster than reconstructing the entire DOM tree every time, an inevitable cost of comparing two DOM trees to find the differences occurs. Therefore, it is slower than manually finding the changes and manually applying those changes. However, because the virtual DOM allows you write the declarative UI codes, you can write shorter and more intuitive UI codes using the reactivity and immutable objects.
Why Preact?
The main goal of the TOAST UI GRID version 4 update was to remove the dependencies that followed the older Backbone and jQuery codes. Therefore, our initial goal was not to use any library at all, and code everything by ourselves. In other words, our plan was to rewrite the state management and DOM manipulation functionalities originally handled by Backbone and jQuery from scratch.
However, we realized that rewriting everything that is simply similar to the original is meaningless. It has been more than four years since the TOAST UI Grid was first developed, and we thought it was paramount that we use the four years of experience to make it better than before. After spending hours brainstorming for ideas for improvement, we decided that declarative programming with virtual DOM like React made the UI code much more concise, and we decided to implement its benefits for the new Grid.
However, implementing a virtual DOM engine like React from scratch was in no way a simple task. React not only compares the virtual DOM trees to minimize DOM manipulation, but also is in charge of diverse functionalities like component’s local state management, event binding, life-cycle methods, JSX, and context. The cost to return ratio of programming everything from scratch for the purpose of using it for a single library was not worth it.
Preact, on the other hand, provides everything we were looking for with unbelievably small size of 8KB when minified, and 3KB when compressed with Gzip. Writing from our experience with React, we were certain that the 8KB of Preact will be much less than the chunk of code that will be replaced with declarative UI codes. Also, because codes using the virtual DOM is much more intuitive and shorter than actually manipulating the DOM, we came to a conclusion that, in terms of maintenance cost, using Preact would yield greater benefits. Lastly, the fact that the team members were already used to React and did not require additional training played a major role in the decision process as well.
There are two, light libraries that provide just the virtual DOM: Preact and snabbdom. However, as a team, we decided that considering the performance, bundle size, usability, project activity, and community size, Preact has the upper hand.
Reactivity State Manager + Preact
Not only does the Backbone provide the template and structure necessary to display the UI, but also provides the event driven state management functionality. However, Preact, on the other hand, only handles the UI, so there were limitations with using just component’s local state management feature found in Preact to manage a complex state like that of the Grid. Despite the limitations, using an event driven state management system like Backbone does not suit Preact’s coding philosophy.
We needed to find a more declarative state manager.
Redux and MobX, widely used in React, are declarative state managers that make used of immutable objects as well as reactivity objects. Such libraries can easily be adopted with Preact, but using more libraries, therefore more dependencies, when we have already compromised and used Preact made us uncomfortable. Not to mention, Grid is built to deal with massive data load, and such characteristics created problems that could not be solved using regular libraries. Therefore, we decided to build a reactivity state management system that is similar to MobX from scratch to use.
There is another article documenting this decision and process, so if you are interested, feel free to check it out!
Integrating the reactivity system manager with Preact was not a difficult task. We designed the state manager to have a single store structure like that of Redux, so all we had to do was to inject the corresponding store to the context and write a Higher Order Component (HOC) called connect
to deliver the necessary values to the component as Props. The connect
function is actually almost identical to the connect function from react-redux, and can be used like the following.
function MyComponent({name, score}) {
return (
<div>Hello {name}, you've got {score} points.</div>
)
}export connect((state) => ({
name: state.player.name,
score: state.player.score
}))(MyComponent);
The implementation itself is equally simple. Our state manager has an observe function which takes a callback function as a parameter. When the callback function is executed, the attributes of observable objects accessed from the inside of the callback function are detected. When the observable objects have changed (or when the change has been detected,) the callback function that we passed as parameter is executed again.
Since all of the values in the single store are observable objects, you can simply call the setState() method with the resulting value of the selector function that will be passed in to the connect function as the parameter from inside of the observe function, and deliver this.state as Props.
export function connect(selector) {
return function(WrappedComponent) {
return class extends Component {
constructor() {
const {store} = this.context; this.unobserve = observe(() => {
const selectedProps = selector(store, ownProps);
if (!this.state) {
this.state = selectedProps;
} else {
this.setState(selectedProps);
}
});
} componentWillReceiveProps(nextProps) {
this.setState(selector(this.context.store, nextProps));
} componentWillUnmount() {
if (this.unobserve) {
this.unobserve();
}
} public render() {
return <WrappedComponent {...this.props} {...this.state} />;
}
};
};
}
Comparing Backbone Codes to Preact Codes
Now, let’s look at actual codes to see what kinds of benefits using the vitual DOM provided by Preact has over the traditional method of directly manipulating the DOM. The example code I have provided is a snippet from the actual TOAST UI Grid that renders tables. For the sake of readability, I have left out some of the unnecessary codes.
Original Code : Backbone (+ jQuery)
The following is the previous code written with Backbone and jQuery.
const TableContainerView = View.extends({
initialize(options) {
this.data = options.data;
this.columns = options.columns;
this.dimension = options.dimension; // (1) Detecting the change event from the model to directly change the UI
this.listenTo(this.dimension, 'change:bodyHeight', this._updateBodyHeight);
}, template: _.template(`
<div class="tui-grid-table-container">
<table class="tui-grid-table"></table>
</div>
`),
_updateBodyHeight(model, bodyHeight) {
this.$el.height(bodyHeight);
}, render() {
// (2) Initial rendering with template
this.$el.html(this.template()); // (3) Add as child nodes after constructing view objects
const colGroupView = new ColGroupView(this.columns);
const tableBodyView = new TableBodyView(this.data, this.columns);
const $table = this.$el.find('tui-grid-table'); $table.append(colGroupView.render().el);
$table.append(tableBodyView.render().el);
return this;
}
});
The code above serves three main tasks (as I have illustrated in comments.) The first (1) purpose is to detect the model change in order to directly manipulate the DOM, and the second (2) purpose is to use the template to perform initial rendering. While template can be helpful in representing the DOM structure in a declarative manner, if the entire DOM is changed using the template every time, it affects the child elements of the DOM as well. For example, rerendering the entire DOM structure just because a height change was detected, it would be inefficient programming. Therefore, when using Backbone for the View, it is a common practice to use codes that directly manipulate the DOM and the template side by side.
The last section (3) creates the View object and adds it to the existing structure as a child. To those who are familiar with the recent lines of component based frameworks, it seems primitive, and because the template engine provided by Backbone is, in fact, incredibly basic, it is difficult to rely exclusively on the template to composite different view trees. Furthermore, because the parent view has to construct the child view, every models that the child node needs to have, must come directly from the parent node. In order to avoid this problem, we generally have to create a separate factory that contains all of necessary models required to build the view.
New Code : Preact
Now, let’s take a look at the new code written using Preact.
function TableContainer({height}) {
return {
<div class="tui-grid-table-container" style={{height}}>
<table class="tui-grid-table">
<ColGroup />
<TableBody />
</table>
</div>
}
}export connect((state) => ({
height: state.dimension.bodyHeight
}))(TableContainer);
First of all, it is clear that the code has been reduced to around half of the original length. Also, the next aspect that stands out is that the new code uses the style object to declaratively assign the height value for the rendering. As previously mentioned, when working with components, you just have to return the virtual DOM tree represented using the JSX every time the view renders, and Preact looks for the difference and applies only the changed aspect to the actual DOM. Therefore, if the height were to change, only the height
value of the style object would be affected, and the child DOM is not affected at all.
The second noticeable difference is that we no longer have to detect the state change of the model with events. Changes in every store value accessed by the parameter for the connect
function, the selector function, is automatically detected. As we have seen before, each change is passed as Props by the setState()
called internally by the component created by the connect
, so in the code above, every time the dimension.bodyHeight
changes, the component is rerendered and shown on the screen.
The last item of interest is the composition between components. Because Preact can use JSX to represent the inter-component relationships with declarative programming, the entire structure of the component tree is easier to grasp. Furthermore, since ColGroup
and TableBody
components acquire the necessary values as Props through their own connect
functions, the parent components no longer have to worry about the values for children components.
Comparing the Final Bundle Size
With the help of Preact, we were able to shorten the length of the entire code by nearly half. However, I have to acknowledge that I have intentionally selected the snippet where the effect of using Preact is the most clear, so it may not be the best comparison. To be more fair, let’s consider the length of the entire code, not just the snippet.
For the most recent version 3.8.0 of TOAST UI Grid, the size of the minified code excluding Backbone, underscore, and jQuery comes out to be 193KB, but the size of the minified code of newly written v4.0.0 excluding Preact is 132KB. There has been around 30% decrease in overall file size.
However, this is yet another one of unfair comparisons. The reason is that the newly written codes contain everything including Backbone’s state management, Underscore’s utility functions, jQuery’s Ajax requests, and all of the features previously handled by external dependencies. In such case, the fairest comparison would be to compare the final bundle size of everything including dependencies.
The v3.8.0, including all dependencies, came close to 327KB when minified and bundled. However, for v4.0.0, the final size of the minified bundle file, including Preact, is only 145KB. Despite having added more features, the final bundle size of the project has been cut in half. Even if you take into consideration that external libraries contain bunch of unused codes, the difference in size is significant.
The decrease in size does not simply mean faster downloads and loads. Implementing the same feature with only half of the code means that the code is twice as powerful, and naturally, the codes are more concise and effective. Furthermore, having less code obviously means less code to maintain, and therefore, the maintenance cost has also been cut in half.
The development stages for the TOAST UI Grid 4 consisted mainly of continuous gawking at how clean and concise compared to the previous code the new codes written in Preact is. Although the initial goal was to merely replicate the existing features, we observed numerous occasions where the new codes outperformed the older codes. In other words, we have not only decreased the maintenance cost, but also have increased productivity of initial drafting.
Comparing The Performances
There is one more thing before I finish this article with the pat on the back for reducing the total bundle size, and it is performance. Previously, I said that working with virtual DOM has to be slower than directly manipulating the DOM element by searching for it directly. Then, does that mean that we have sacrificed performance of the new Grid for size?
Spoiler alert! Not at all. Preact, much like React, uses the shouldComponentUpdate
method to block unnecessary rendering from happening. Furthermore, although it was not mentioned in the previous example codes, if we set the setState
to be called from inside of the connect
only if the return value of the selector function is different from before, we can prevent unnecessary virtual DOM comparisons without much effort.
As such, by adding a few lines of code to optimize the program, you only have to compare a negligible amount of the virtual DOM tree. Of course, the virtual DOM may use slightly more memory or put extra pressure on the garbage collector compared to using the actual DOM directly. However, this difference is insignificant enough that users will not be able to tell the difference.
The one Achilles’ heel the virtual DOM has is when it has to deal with frequently changing massive arrays or deep trees. Because Grid serves to handle massive arrays, using a virtual DOM could be troublesome. One possible solution to this is using a virtual scroll (rendering only what can be seen on the screen, while creating empty spaces to fill the scroll bar and display relative positioning.) With virtual scroll, because it is very unlikely that the screen has to display more than 30 rows, displaying 30 some arrays does not affect the performance all that much.
Virtual scrolling is actually one of the oldest feature supported by TOAST UI Grid. If you use a virtual DOM, virtual scrolling becomes even easier to implement. The code below, while simplified for the sake of explanation, is a piece of code that implements virtual scroll within the component without the changing the store state portion of the code.
function TableBody({rows, columns}) {
return (
<tbody>
{rows.map((row) => (
<BodyRow
key={row.rowKey}
rowData={row}
columns={columns}
/>
))}
</tbody>
}
}export default connect(({ viewport }) => ({
rows: viewport.rows,
columns: viewport.columns
}))(TableBody);
viewport
is an object that contains objects of the currently visible columns and rows as arrays. If you apply the codes above, the scroll changes, and every time the rows
array and columns
array in viewport
changes, the TableBody
is rendered again. Then, Preact compares the current virtual DOM tree to the previous virtual DOM tree to apply the changes in rows and columns to the actual DOM directly.
Previously, we had to manually code something that is almost identical to what is now done by Preact. However, by using the virtual DOM, the DOM manipulation code required to implement virtual scroll is practically free of charge. Therefore, in v4.0.0, we were easily able to implement the columnwise virtual scroll that was not available before, and it made the Grid even faster.
Summary
Right now is the golden age of frameworks. Most applications are Single Page Applications (SPA,) and it has become difficult to find an application that does not use one of React, Vue, or Angular. In these times, building a UI library means making some hard decisions — whether to use the framework or not.
Writing a library that is dependent on one of the frameworks means that you can make use of different features offered by the framework and focus mainly on the UI codes. However, it also means losing out on many users who use different frameworks.
If you create a library that is independent of any frameworks, you can provide different wrappers for different frameworks, whether it be React or Vue, to appeal to all JavaScript users. However, you would have to manually resolve issues having to do with state management, data binding, template engine, DOM manipulation, and etc., and you run the risk of creating a heavy library with unessential codes. Then, your library loses its edge over other libraries that are specialized to particular frameworks.
Preact can be a holy grail for such dilemmas. It is light, fast, and familiar to React users. It is true that we had our doubt when we were first trying to introduce Preact to our project because of our uneasy feeling of having to rely on another library and a little bit of doubt of the results. However, as you have seen so far, we were able to achieve unprecedented results that easily topped our wildest expectations, and without Preact, it would not have been possible.
In fact, because TOAST UI Grid does not explicitly list Preact as an external dependency and comes included with Preact in the bundle, users don’t even have to know that they are using Preact. Ultimately, users can use a new version of a library that is free of external dependencies at a smaller size. If there are other developers out there who are pulling out their hair over similar issues, we encourage you to give Preact a shot.
TOAST UI Grid 4 comes with many more changes than mentioned in this article. Entire codebase has been written based on TypeScript, and have added new specs like custom renderer and custom editors so that users can use the product more flexibly and extensibly.
All of the changes and future release information have been carefully documented in the official release note. We encourage you to take a look, and hope that you stay up to date with TOAST UI Grid for more exciting experiments!