Glimmer-VM: a deep dive

  As the README says, Glimmer is a render engine built on top of Handlebars to pave a for a rendering pipeline, in browser. The concept or Component used by Ember originates here. This is a low-level implementation of the declarative templating library. Let me explain that a bit. By low-level, we mean that it will have a compiled version of template called wire-format with concepts of OpCodes, Register etc.

  The VM handles the core UI rendering and update mechanisms. It is not tied to any particular framework and can be used independently. It consists of mainly two parts: the compiler and the runtime, we will discuss shortly.

Ember.js affiliation

It is shipped to client side in ember.js, but not for the usecase of Server rendered pages. Ember.js uses Glimmer-VM as its rendering engine to handle the rendering and updating of user interfaces in the browser. When you build an Ember.js application, the necessary parts of Glimmer-VM are included in the JavaScript bundle that is shipped to the client's browser.

Ember.js builds on top of Glimmer-VM, adding a structured application framework with conventions, routing, and various features for building web applications. When you develop an Ember.js application, the VM takes care of rendering and updating the DOM based on your application's state and changes to that state.

The Compiler

This section will have a short intro on the compiler stage, but a detailed description is given here. The main function is to convert the Handlebars template into a wire-format which can be shipped to the client side, to be rendered efficiently by the @glimmer/runtime.

Rendering and Updating

When a Glimmer-powered application loads, the VM is responsible for rendering the initial view. It uses the compiled bytecode to render the template into a virtual representation of the UI. Glimmer being a reactive framework, the VM knows which parts of the UI need to be updated when the data changes and efficiently updates only those parts. This process is often referred to as the "reconciliation" phase. It uses a diffing algorithm (similar to what is popularized by react.jsto compare the virtual representation of the UI (created from the bytecode) with the actual DOM (Document Object Model). It identifies the differences between the two and updates only the parts that have changed.

Glimmer is known for its performance optimizations. It reduces the number of DOM manipulations by batching updates, using a virtual DOM-like approach, and making use of fine-grained change tracking. It can do partial rehydration, where it only renders the parts of the UI that have changed. This is particularly useful for server-side rendering (SSR) and initial page loads, as well as incremental rendering.

Global Context

Serves as a way to provide a common data and execution environment for your templates and components. It enables data binding, allows components to access shared services, and ensures that your user interface remains consistent with the underlying application state. Let us see how the global context works in the Ember.js ecosystem, which includes the Glimmer VM:

In Ember.js, the global context is primarily managed through its core concepts of Services and State Management. In Ember.js, services are singletons that provide a way to share functionality and data across different parts of your application. Services are part of the global context and are accessible from any component or route. Some examples are encapsulating common functionalities such as user authentication, data fetching, or real-time communication etc. They can be injected into components, routes, and other services, making them easily accessible for sharing data and behavior.

main.js

// Create a service
import Service from '@ember/service';

export default class UserService extends Service {
  user = { name: 'John' };
}

// Use the service in a component
import Component from '@glimmer/component';
import { inject as service } from '@ember/service';

export default class UserProfileComponent extends Component {
  @service user;

  get userName() {
    return this.user.name;
  }
}

Next, the State Management, where Ember.js employs a data-down, actions-up (DDAU) architecture, which means data flows from the parent to child components via arguments, and actions are used to communicate changes back up. This can be observed as part of many modern SPA frameworks (excluding Angular v1 etc). The global context often includes the top-level data and state for your application, and it can be made available to child components through arguments. For example, a top level route or a top-level component that manages application-wide data and passes it down to child components:

main.js

// Parent component or route
import Component from '@glimmer/component';

export default class ApplicationRoute extends Component {
  appState = {
    theme: 'light',
    user: { name: 'Alice' },
  };
}

HBS Template to pass to child:

template.hbs

<!-- Child component template -->
<MyComponent @appState={{this.appState}} />

This provides a way to share data and functionality across your application while maintaining a clear and structured architecture. It promotes maintainability and reusability by allowing components to access the data they need and communicate with services, all within a consistent global context.

The code comment says:

This package [@glimmer/global-context] contains global context functions for Glimmer. These functions are set by the embedding environment and must be set before initial render.

source

In Ember.js, the context is set here using setGlobalContext exported from the glimmer package. All these are part of environmental initializations covered which is covered here.

References