The Famo.us Render Tree
One of the first things to notice about Famo.us is how little
we expose HTML and the DOM to the developer. Interacting with the DOM is riddled
with performance issues. Famo.us abstracts away DOM management by maintaining a
representation of it in JavaScript called the Render Tree.
If you inspect a website running Famo.us, you'll notice the DOM
is very flat: most elements are siblings of one another. Inspect any other
website, and you'll see the DOM is highly nested. Famo.us takes a radically
different approach to HTML from a conventional website. We keep the structure of
HTML in JavaScript, and to us, HTML is more like a list of things to draw to the
screen than the source of truth of a website.
Developers are used to nesting HTML elements because that's the way to get
relative positioning, event bubbling, and semantic structure. However, there is
a cost to each of these: relative positioning causes slow page reflows on
animating content; event bubbling is expensive when event propagation is not
carefully managed; and semantic structure is not well separated from visual
rendering in HTML.
Famo.us promises a rich 60 FPS experience, and to do so, we needed to circumvent
these inefficiencies. When we decided to abstract away the DOM, we needed a way to maintain
the expectations every web developer has of the DOM, but in a way that doesn't compromise on
performance. The Render Tree is our solution to relative positioning and
semantic structure. In other documentation we'll go into events and animation.
Overview
Creating the Tree
A tree's starting point is called its root. In HTML, this root is the <body>
tag.
In Famo.us, the root is a Context. We instantiate a Context via the
Famo.us Engine's .createContext
method. This will create a <div>
with CSS
class famous-container
(we can also pass in a pre-existing DOM element).
context var context = Engine.createContext();
│
Extending the Tree
So far we have a pretty boring app. A Context has no visual representation,
it merely provides a starting point for Famo.us' render cycle. To get something
on the screen, we will need to extend the Render Tree by adding nodes via the.add
method. A Famo.us Surface is one kind of node, which loosely
corresponds to a <div>
in HTML. This <div>
will be nested inside of the<div>
allocated to the Context. This is how we build up HTML in Famo.us.
context var context = Engine.createContext();
│
surface context.add(surface);
Types of Nodes
A tree is made up of nodes. In HTML, these nodes are tags like <div>
or<button>
. In Famo.us, nodes come in two flavors: renderables, and modifiers.
Above, we saw how to add one type of renderable, a Surface, to the Render Tree.
Below, we will examine other kinds of nodes that make up a typical Render Tree.
Renderables
Renderables are nodes that get drawn to the screen. We have already been
introduced to a Surface, which is associated with an HTML <div>
, but there are
other kinds of surfaces associated with other HTML tags. Famo.us currently supports:
Surface Type | Associated Tag |
---|---|
Surface | <div> |
ImageSurface | <img> |
InputSurface | <input> |
CanvasSurface | <canvas> |
VideoSurface | <video> |
There's one more surface type, called a ContainerSurface, which is associated
to a <div>
that nests a Surface within it. This is used primarily for clipping
when {overflow : hidden}
is set as a CSS property.
All surfaces can take in arbitrary HTML content and CSS styling. Famo.us is 100%
agnostic to what you do within a Surface, whether it's render from a template,
or use an MVC to bind data to its content. However, if you want to independently
animate a chunk of HTML, or bind DOM listeners that interact with the rest of
your app, we suggest you encapsulate that inside a Surface; a surface's content
is reserved for HTML that is static, or at least doesn't update often.
Surfaces are the atomic renderable unit in Famo.us, but we also support more
complex composited renderables. These are called Views and will be
discussed below.
Modifiers
A Modifier is another type of Famo.us node that is capable of modifying
the nodes below it in the Render Tree. Surfaces are dumb, really. They don't
know where they are in the page, or whether they're even visible; that is
the job of the Modifier. Modifiers are responsible for the layout and visibility
of the Render Tree below them. We group these two different concepts together
because CSS3 transforms and opacity are precisely the hardware accelerated
properties that can change performantly.
context var context = Engine.createContext();
│
modifier var chain = context.add(modifier);
│
surface chain.add(surface);
The above example is a stripped down version of how a Modifier is applied to
a Surface. For instance, if we define the modifier with
var modifier = new Modifier({
transform : Transform.translate(100,200)
});
then the surface will be at [100px, 200px]
from the top left of the Context.
Modifiers also have more complicated support for layout, like auto-centering
and sizing, but this will be discussed in a different tutorial.
Chaining Nodes
Modifiers affect the render tree beneath them. But that means Modifiers can
affect other Modifiers. By chaining Modifiers, their effects compound: their
transforms are composed, and their opacities are multiplied. This makes
separating state easy to do. One modifier can handle opacity, another, rotation.
context var context = Engine.createContext();
│
modifier1 context.add(modifier1)
│ .add(modifier2)
modifier2 .add(surface);
│
surface
Branching Nodes
So far our Render Trees have been linear: one node sequentially following another.
What makes trees fun is when they branch. Below is a simple example demonstrating
how to branch the tree by calling .add
successively on the same node.
context var context = Engine.createContext();
┌─────┴─────┐
modifier surface2 context.add(modifier).add(surface1); // left branch
│
surface1 context.add(surface2); // right branch
Branching is key to relatively positioning renderables. For example,
context var context = Engine.createContext();
│
modifier1 var relativeNode = context.add(modifier1);
┌─────┴─────┐
modifier2 surface2 relativeNode.add(modifier2).add(surface1);
│
surface1 relativeNode.add(surface2);
Here, surface1
and surface2
are both relative to modifier1
, andsurface1
has an additional modifier so that the surfaces are non-overlapping
(assuming these modifiers are for translating nodes).
Views
So far we've seen that you can add Modifiers and Surfaces to the Render Tree.
These can be thought of as the lego blocks of more complicated widgets. To help
reduce the boiler plate to creating a widget, Famo.us provides a base class
called a View. A View provides an interface for adding to the Render Tree (no different
from a Surface or Modifier), receive and broadcast events, and take in default parameters
and state variables. Famo.us also ships with a library of common views that we hope to
keep building upon (and hope our community contributes to!). We will consider how
Views handle events and state in other documentation. Here, we are only concerned with
how they can be used to extend the Render Tree, and how they encapsulate their own
internal Render Trees.
In the example below, we add a Scrollview.
context var context = Engine.createContext()
│
modifier context.add(modifier).add(scrollview);
│
scrollview
Internally, Scrollview has its own complex logic, but that is hidden from the
developer, who can simply include it in her project by adding it to the tree
like any other node. It's the Famo.us equivalent of the
Shadow DOM.
After instantiating a Scrollview, we can populate it with any other renderables
via its sequenceFrom
interface, thus building its internal Render Tree.
scrollview scrollview.sequenceFrom([S1, S2, S3, ... , S10]);
┌───┬───┼───────┐
S1 S2 S3 ⋯ S10
Notice in the above example, S10 doesn't have to be a Famo.us Surface; it could
have been a view with its own modifiers, other nodes, even another scrollview.
You could have a scrollview whose first item has a cross-fading opacity between
two surfaces by letting S10 be its own View with the structure:
S10 S10.add(modifier1).add(surface1);
┌─────┴─────┐
modifier1 modifier2 S10.add(modifier2).add(surface2);
│ │
surface1 surface2
If we were to unravel the Render Tree, we would find it looks like:
context
│
modifier
│
scrollview
┌───┬───┼───────┐
S1 S2 S3 ⋯ S10
┌─────┴─────┐
modifier1 modifier2
│ │
surface1 surface2
but by encapsulating complex logic in views, understanding an app becomes more
manageable. And unlike DOM, there is no performance degradation incurred from
nesting structure; everything is flattened by the time it gets to the DOM.
The Big Picture
It's modifiers all the way down - Anon
In all the examples above, you'll notice a pattern: a Render Tree starts with
a Context, branches into a bunch of Modifiers, and ends with Surfaces. Unlike
the DOM, where nodes mix visual representation with syntactic clustering, the
Render Tree makes a clear separation between layout (Modifiers), content
(Surfaces) and structure (.add
).
In fact, if you want to know what the position, or opacity of a surface
at the bottom of the Render Tree, you just have to multiply the opacities and
transforms of the Modifiers above it.
Another point of divergence is that the DOM executes a redraw whenever a node's
styling or content is changed (immediate mode). In Famo.us, the Render Tree
batches changes (retain mode) behind the scenes by buffering them against therequestAnimationFrame
API. This ensures changes are resolved at the most optimal time
(synced to your monitor's refresh rate).
To recap, here's a comparison between traditional DOM, and the Famo.us Render Tree.
Famo.us Render Tree | DOM | |
---|---|---|
Tree Structure | Yes | Yes |
Nodes | Renderables & Modifiers | HTML Elements |
Reflows | No | Yes |
Encapsulation | Views & Widgets | Shadow DOM |
Meaning | Structure | Structure, Rendering |
Render Cycle | Retain Mode | Immediate Mode |
Language | JavaScript | HTML |