Solarite

Solarite is a small (9KB min+gzip), fast, compilation-free JavaScript library for enhancing vanilla web components with minimal DOM updates on re-render.

Key Features

Installation

Quick Start

Import the module directly from a CDN:

Or install via NPM:

Development Tips

For the best development experience, use an IDE like WebStorm or VS Code with a Lit-html extension for syntax highlighting of HTML template strings. Solarite's included Solarite.d.ts provides auto-completion and type checking for all core APIs.

Performance

Solarite provides near-native performance by performing targeted DOM updates. Benchmark is un on a Ryzen 7 3700X on Windows 10. Performance is still improving.

js-framework-benchmark

Note that the JS Framework Benchmark separates keyed and non-keyed frameworks. Solarite is non-keyed according to the criteria of this benchmark but in this chart it's placed next to keyed frameworks since otherwise we can't compare it with the most popular frameworks.

Core Concepts

Web Components

Solarite enhances web components with efficient and minimal re-rendering of elements when your data changes. This approach minimizes DOM operations and improves performance.

In this minimal example, we create a class called MyComponent which extends from HTMLElement (the standard way to create web components). We add a render() method to define its HTML content, and call it from the constructor when a new instance is created.

Important: All browsers require web component tag names to contain at least one dash (e.g., my-component, not mycomponent). This is a standard requirement for custom elements.

We can alternatively instantiate the element directly from html:

Since these are just regular web components, they can define the connectedCallback() and disconnectedCallback() methods that will be called when they're added and removed from the DOM, respectively.

Rendering

How Rendering Works

The h function, when used as a tagged template literal, converts HTML and embedded expressions into a Solarite Template. This data structure efficiently stores processed HTML and expressions for optimal rendering.

When you call h(this) followed by a template string, it renders that Template as the element's attributes and children. Conceptually, this is similar to assigning to the browser's built-in this.outerHTML property, but with a crucial difference: Solarite's updates are much faster because only the changed elements are replaced, not all nodes.

Manual Rendering

Unlike many frameworks, Solarite does not automatically re-render when data changes. Instead, you must call the render() function manually when you want to update the DOM. This is a deliberate design choice that:

  1. Gives you complete control over when rendering occurs

  2. Reduces unexpected side effects

  3. Allows you to update internal data without triggering a render

  4. Makes application behavior more predictable

This approach is particularly useful in performance-critical applications where you need precise control over when DOM updates occur.

Wrapping the web component's html in its tag name is optional. But without it you then must set any attributes on your web component manually, as seen in this example:

If you do wrap the web component's html in its tag, that tag name must exactly match the tag name passed to customElements.define().

Note that by default, h() will render expressions as text, with escaped html entities. To render as html, wrap a variable in the h() function to create a template:

Folder icon comes from Google.

These types of objects can be returned by in expressions with h tagged template literals:

  1. strings and numbers.

  2. boolean true, which will be rendered as 'true'

  3. false, null, and undefined, which will be rendered as empty string.

  4. Solarite Templates created by h-tagged template literals.

  5. DOM Nodes, including other web components.

  6. Arrays of any of the above.

  7. Functions that return any of the above.

Attributes

Dynamic attributes can be specified by inserting expressions inside a tag. An expression can be part or all of an attribute value, or a string specifying multiple whole attributes. For example:

Expressions can also toggle the presence of an attribute. In the last div above, if isEditable is false, null, or undefined, the contenteditable attribute will be removed.

You can also specify multiple attributes at once using an object, where the keys are attribute names and the values are attribute values:

In the example above, all attributes from the this.attrs object are applied to the button element. If a value is undefined, false, or null, the attribute will be skipped or removed if it was previously set.

Note that attributes can also be assigned to the root element, such as class="big" on the <attribute-demo> tag above.

Id's

Any element in the html with an id or data-id attribute is automatically bound to a property with the same name on the class instance. But this only happens after render() is first called:

Id's that have values matching built-in HTMLElement attribute names such as title or disabled are not allowed.

Events

To intercept events, set the value of an event attribute like onclick to a function. Alternatively, set the value to an array where the first item is a function and subsequent items are arguments to that function.

Event binding with an array containing a function and its arguments is slightly faster, since the function isn't recreated when render() is called, and it doesn't need to be unbound and rebound. But the performance difference is usually negligible.

Make sure to put your events inside ${...} expressions, because classic events can't reference variables in the current scope.

Two-Way Binding

Two-way binding creates a connection between your component's data and form elements, so changes in either one automatically update the other. This is particularly useful for forms and interactive UI elements.

Basic Two-Way Binding

Form elements can update the properties that provide their values when an event attribute such as oninput is assigned a function to perform the update:

<input>, <select>, <textarea>, and elements with the contenteditable attribute can all use the value attribute to set their value on render. Likewise so can any custom web component that defines a value property.

Shorthand Two-Way Binding

Solarite also provides a shortcut for two-way binding using array syntax: value=${[this, 'count']}:

  1. When render() is called, the input's value is set to this.count

  2. When a user types in the input, an input event listener updates this.count with the new value.

Optionally add an oninput=${this.render} attribute to trigger re-rendering when the value changes.

Loops

The most common way to render lists is with JavaScript's Array.map() function:

Efficient List Updates

When you change an element or add another element to the items list and call render(), Solarite only redraws the changed elements.

Important: Nested template literals must also have the h prefix. Without this prefix, they'll be rendered as escaped text instead of HTML elements.

Scoped Styles

Solarite provides a powerful scoped styling system that allows components to define styles that apply only to themselves and their children, while not using Shadow DOM so that styles are inhereted from the rest of the document.

When you include a <style> element in your component template, Solarite automatically scopes those styles to your component instance. This prevents style leakage and conflicts with other elements.

Internally, scoped styles become:

  1. A unique data-style attribute to the root element with an incrementing data-style attribute for each component instance

  2. :host selectors are replaced with the web component tag name and unique identifier: fancy-text[data-style="1"])

A style tag with the global attribute defines the style only once in the document head, instead of for every instance of a component. This improves rendering performance with many instances. Global styles cannot have expressions within them.

Child Components

Solarite makes it easy to compose complex UIs by combining smaller, reusable components.

Passing Data to Child Components

When one web component is embedded within the html of another, its attributes and children are automatically passed as arguments to the constructor:

Attribute Name Conversion

Since HTML attributes are case-insensitive, Solarite automatically converts dash-case (kebab-case) attribute names to camelCase when passing them to component constructors. For example, the font-size attribute becomes the fontSize property of the component constructor and render methods first argument.

Component Rendering Hierarchy

When calling render() on a parent component:

  1. The parent component renders its template

  2. For each child component in the template, Solarite calls that child's render() method

  3. The child component receives the new attributes as an object in the first argument of its render() function

  4. The child component can compare these attributes with its current state and decide whether to call h() to check if anything needs to re-render.

  5. If the child component calls h(this), the process continues if that child component has child components of its own.

This allows each component to control its own rendering while maintaining a predictable data flow.

In the above code, we alternatively could've created the <notes-item> element via the new keyword, but this is ill-advised. Doing so would cause all NotesItem components to be recreated on every render:

The h() Function

The h() function handles template creation, DOM updates, and element instantiation:

Advanced Techniques

Extending Native HTML Elements

HTML has strict rules about which elements can be children of certain container elements. For example, a <table> can only have specific children like <tr>, <thead>, etc.

If you want to create a custom component to use in these restricted contexts (like a custom <tr> element), you can extend the appropriate native HTML element instead of the generic HTMLElement.

To do this, pass the native element constructor as the third argument to customElements.define. This is standard, vanilla JavaScript and is not specific to Solarite.

Manual DOM Operations

While Solarite handles most DOM updates automatically, there are cases where you might want to perform manual DOM operations for specific optimizations or integrations with third-party libraries.

You can safely perform manual DOM operations in these scenarios:

  1. Attribute Modifications: You can modify any attributes that were not created by expressions, on any nodes that were not created by expressions.

  2. Node Addition/Removal: You can add or remove nodes that meet all these criteria:

    • Not created by an expression

    • Not positioned directly before or after an expression that creates nodes

    • Do not have any attributes created by expressions

  3. Temporary Modifications: You can modify any node temporarily, as long as you restore its previous position and attributes before render() is called again.

Following these guidelines ensures that Solarite's rendering system continues to work correctly alongside your manual DOM operations. This example creates a list inside a div element and demonstrates which manual DOM operations are allowed:

Regular Elements

The toEl() function can create html elements:

You can also pass objects to toEl() with a render() method. This object can optionally have additional properties and methods, which become bound to the resulting element. When render() is called, only the changed nodes will be updated.

If you want multiple instances of such an element, the code above can be wrapped in a function:

How Solarite Works

Understanding how Solarite works internally can help you write more efficient components and debug issues more effectively.

Efficient Rendering Algorithm

Consider this example where we're rendering a list of tasks:

When you call render(), Solarite performs these steps:

  1. Template Parsing: The h() function processes the template literal, separating static HTML from dynamic expressions.

  2. Expression Hashing: Solarite creates a hash of every ${...} expression's value. This allows it to quickly identify which expressions have changed since the last render.

  3. Differential Rendering: By comparing the current hashes with those from the previous render, Solarite determines exactly which DOM elements and attributes need to be updated.

  4. Minimal DOM Updates: Only the elements and attributes with changed values are modified in the DOM, leaving everything else untouched.

DOM Diffing

For efficient list updates, Solarite uses WebReflection/udomdiff, a lightweight and fast algorithm for comparing and updating DOM nodes. This ensures that list operations (adding, removing, or reordering items) are performed with minimal DOM manipulations.

Examples

This is the time example from Lit.js implemented with Solarite:

Upcoming Features

Solarite is actively being developed with several exciting features planned for future releases:

  1. Shadow DOM Support: Optional integration with the browser's native Shadow DOM for true encapsulation of styles and DOM.

  2. JSX Support: Alternative syntax for those who prefer JSX over template literals.

  3. Automatic Rendering: An opt-in feature to automatically re-render components when watched properties change, eliminating the need to manually call render().

  4. Performance Optimizations: Continued improvements to rendering speed and efficiency.

Stay tuned for updates on these features by following the GitHub repository Star.