Skip to content

Instantly share code, notes, and snippets.

@lancejpollard
Last active August 29, 2015 13:57
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save lancejpollard/9754522 to your computer and use it in GitHub Desktop.
Save lancejpollard/9754522 to your computer and use it in GitHub Desktop.
The Client-Side Rendering Problem

The Client-Side Rendering Problem

The core problem with UI rendering boils down to these two questions:

  1. How do you know when something has changed?
  2. How do you most optimally update the UI with those changes?

What has changed?

There are several implementations that can tell you what has been changed, some of which are:

  1. getters/setters that hook into when you set a property
  2. wrapper apis that act as getters/setters so you can hook into when you set a property
  3. dirty checking, where you have a current and previous set your data, and compare to see what is different
  4. immutable dirty checking, where you recreate the data, and compare that to the last recreated data

How to update the UI?

What is the way to most optimally update the UI with those changes?

Nobody knows the most optimal answer to this question yet.

The variables involved in solving this problem most optimally are:

  • minimizing the amount of memory being consumed
  • minimize the change in memory consumed (so memory usage can ideally stay constant)
  • minimize the drawing required, by only redrawing what is absolutely necessary.

That last one is the meat of the problem: how to know just the essentials that need updating, without overcomplicating the way you write the UI code.

Keys to an optimal solution

Some of the key parts of the optimal solution include:

  • To minimize memory, you have to use mutable objects, because this way there is a finite amount of memory that is constantly reused. But how do you do this in an easy way, that is as simple as the immutable object approach like Facebook React.
  • The API has to be at the lowest level, using plain JavaScript objects. This way you can write your code without any knowledge of the implementation, and just plug your object into the renderer. It also means you're getting the most performance possible.
  • The minimal amount of code has to be executed, during updates and at startup.
  • The API must be simple, so it can be adopted by teams used to legacy code and resistant to change.

Bonus points would be a solution that can adapt to new findings in rendering performance optimizations. So if you could rewrite the renderer, without having to rewrite all the other UI components and such, that would be ideal.

It's also possible that the solution could be generalized to work across any graphics system, like the canvas, SVG, HTML, webgl, even the terminal or some sort of ascii UI.

Implementation details

Each framework and implementation so far has pros and cons (Backbone, Ember, Angular, React). Often if you use one of these frameworks and run into a performance problem, you can write your code slightly differently and fix it. But that more often than not means that the solution isn't quite right. Something was missing from really nailing it.

The getter/setter approach

var section = {};

Object.defineProperty(section, 'title', {
  get: function() {
    return this._title;
  },

  set: function(val) {
    var prev = this._title;
    this._title = val;
    this.emit('change title', val, prev);
    return val;
  }
});

section.title = 'The getter/setter approach';

Problems with the getter/setter approach is that it:

  • creates magic by overriding simple properties and doing unknown things.
  • it decreases performance, especially for things that are updating every frame (ballpark ~50% in some cases) if you are emitting events or even just simple things like keeping track of the previous value.
  • it requires extra code execution at startup.

This approach also doesn't work with <IE9.

The wrapper getter/setter approach

This approach is nice because you can get pretty low-level performance gains. But, it requires you write your code in a specific way. Backbone and Ember both do this.

var Section = Backbone.Model.extend();
var section = new Section;

section.get('title');
section.set('title', 'The wrapper getter/setter approach');

You could also do it like this, which is better than the previous approach because you don't have to extend custom objects like in Ember and Backbone:

get(section, 'title');
set(section, 'title', 'The wrapper getter/setter approach');

But again, this still changes the way you have to write all your code and isn't feasible. People don't write games like this. And the goal is to make the code execute as fast as possible, like page.title = x.

The dirty-checking approach

This is Angular's approach. This is a very simple approach that's easy to understand, but it doesn't perform well when you have lots of data such as in lists, or when you have lots of properties. Basically, the more properties you have, the slower it gets.

All that happens is they have a copy of the tree of data in your DOM, and then every frame/interval they compare every property in their copy with the actual tree of data. When something is different, then they update the DOM. This minimizes the amount of DOM operations, but it comes at the cost of doing unecessary work most of the time.

A benefit to this approach though, is that you can write your JavaScript using plain objects, which is the ideal.

section.title = 'The dirty-checking approach';

The immutable dirty checking approach

This is Facebook React's approach. They are still doing dirty checking like angular, but they are doing it in a much more optimized way. Instead of comparing every property of your data tree with their previous values every frame, it only does it when you change some property in a component. This:

  1. Isolates the number of changes to be made to the number of properties contained in the component and all its child components, and
  2. Only computes the difference when there is actually a change to the data, not every frame.

This is why Facebook React is among the fastest approaches right now.

However, it still is missing several key features of the ideal solution:

  1. It requires you to use their object/class system, and write all your code around that
  2. Even though the updates are more optimized than Angular because they are isolated to a specific component, if you only change one property you have to re-render the entire component.
  3. The usage of the "immutable data" for rendering is nice because it's simple and easy to understand, but it signifantly effects peformance when updates are happening every frame.

For (2), imagine a grocery list where you have the name of the current item listed at the top, and underneath a list with the names of 20 grocery items. Using Facebook React's approach, it is easy to build something where you have a single component, and when you select the item so it displays the name on the top, you still have to recreate all of the child objects (ReactComponent), just for that one single change. Again, you can often refactor your code into sub-components to fix this, but that's not always ideal.

For (3), imagine a game, where you are moving a bunch of objects on the screen. To make this most optimal, you should reuse all of the objects and minimize the changes to memory usage as much as possible. So using immutable objects here is not ideal. If you create this game using Facebook React, you would constantly be recreating all the objects in the game, which would be super slow. Angular would probably perform much better in this case (at least in principle).

The ideal solution

Maybe there is no ideal solution. Maybe you use immutable objects like Facebook React on DOM elements to render UIs that don't change much (like most apps nowadays), and use mutable objects for games. Maybe it's like that saying, whatever it is: find a solution that fits the problem.

But there is probably something better. Possibly a solution that would:

  • allow you to write in simple, plain JavaScript
  • make it easy to debug and extend large apps with complicated UI
  • render faster than anything that's out there right now could

The key questions to consider are:

  1. What is the best way to know when something has been changed?
  2. What is the most optimal way to update the UI with those changes?
@goldoraf
Copy link

  1. Use Event Sourcing on the client AND the server : http://martinfowler.com/eaaDev/EventSourcing.html
    It's better for your domain model, and changes are domain events, no need to compare sets of data.
  2. Start with views that rerenders with each domain event ; optimize when necessary at the view-level : different views always need specific optimizations.

My 2 cents ;)

@lancejpollard
Copy link
Author

There could be a few different renderers for the different browser environments. For old IE, it could just do dirty checking on the entire tree like angular. For browsers with Object.observe, you could specify the properties to observe for each component in a map on the renderer, so it would just update the bare bones. On browsers in between perhaps, you could have a renderer that overrides with getters/setters. For reverse binding from the DOM to the object, we could use potentially MutationObserver for newer browsers. This would be super simple to plug objects into.

By renderer I'm thinking like how a graphics engine would do it, running through a "display list" for the canvas / dirty checking case, and simply attaching observers for the HTML case. Something like:

https://github.com/graphicsjs/canvas-renderer2d/blob/master/index.js
https://github.com/graphicsjs/svg-renderer2d/blob/master/index.js

var renderer = require('html-renderer');

renderer.observe(View1.prototype, 'title');
renderer.observe(View1.prototype, 'subtitle');
renderer.observe(View2.prototype, 'x');
renderer.observe(View2.prototype, 'y');
// ... a map of properties that will update the DOM.
// could be automatically done by mapping template.attrs to the renderer.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment