Back

Exploring Million.js, a high-performance web framework

Exploring Million.js, a high-performance web framework

In recent years, the web development landscape has seen a remarkable evolution, witnessing the emergence of more efficient, faster, and user-friendly frameworks. While React remains the most beloved front-end framework, Svelte and Solid have introduced significant performance advantages, enabling the creation of even faster web applications. As the web framework ecosystem continues to progress, a new contender, Million.js, has emerged, and this article will tell you all about it.

First, let’s understand exactly what Million.js is and how it works from a higher level. In this roundup, I’ll guide you through it, exploring its key features, principles, and performance capabilities. We’ll delve into writing code using Million.js and unravel real-world use cases where it shines. By the end of this post, you’ll have a comprehensive understanding of Million.js and its potential to revolutionize the front-end landscape.

Million.js is a minimalistic JavaScript framework designed to handle DOM elements efficiently. Unlike traditional frameworks that update the entire DOM on every state change, Million.js uses a granular approach by updating only the necessary parts of the DOM. This unique strategy drastically reduces the amount of memory usage and enhances rendering speed, making it ideal for any web application dealing with large datasets or complex UI components.

At its core, Million.js employs a fine-grained system of higher-order components called “Blocks.” Imagine these “Blocks” as a high-performant component wrapper for your regular React components optimized for rendering.

Wrapping React Components in Blocks

Any component wrapped in this “Block” component uses Million.js optimized mechanism for re-rendering data, leading to smooth and fast rendering cycles. Million.js also provides the <For/> component for facilitating the rendering of lists.

Key features and principles of Million.js

We talked about how Million.js uses a different and optimized mechanism for rendering, so let’s unravel that mechanism and understand some of the key principles of Million.js.

Key Features and Principles of Million.js

Virtual DOM Diffing

At the core of Million.js’s rendering mechanism lies “Virtual DOM Diffing.” Diffing refers to comparing nodes in the DOM tree to identify which nodes have changed. Frameworks like React and Svelte have their own mechanisms to update the DOM with the latest changes efficiently.

However, Million.js introduces a distinctive approach using a Block virtual DOM instead of the traditional virtual DOM employed by React. The Block virtual DOM is based on the concept of Blockdom, which facilitates virtual DOM diffing for re-rendering the actual DOM.

Let’s take a simple example of a DOM tree and walk through the diffing process using Million.js’s approach. To grasp this concept, let’s break it down into two essential steps - Static Analysis and Dirty Checking.

Let’s understand the above steps using a simple example. Consider the following initial DOM tree:

<div id="app">
 <h1>Hello, World!</h1>
 <p>This is a Million.js example.</p>
 <button>Click me</button>
</div>

Here’s a simplified diagram representing the DOM tree:

Initial DOM Tree

Now, let’s assume the user interacts with the page, triggering a state change that updates the content of the <h1> element:

<div id="app">
 <h1>Welcome to Million.js!</h1>
 <p>This is a Million.js example.</p>
 <button>Click me</button>
</div>

Let’s see what happens in the first step of the diffing process.

Static Analysis

In this initial step, the virtual DOM is interpreted to create an “Edit Map.” This “Edit Map” is essentially a set of key-value pairs representing the virtual DOM’s dynamic parts. These dynamic parts are typically associated with the application’s state, which can change over time based on user interactions or data updates.

Here’s the associated “Edit Map” used for State Change in our example:

{
 "h1": "Welcome to Million.js!"
}

The initial virtual DOM is analyzed to create the “Edit Map” representing the dynamic parts of the virtual DOM. In this case, the “Edit Map” records that the content of the <h1> element has changed from “Hello, World!” to “Welcome to Million.js!

Dirty Checking

In the next step, after the “Edit Map” is created, it is utilized to directly update the DOM if the state has changed.

With the “Edit Map” in hand, Million.js now compares the recorded changes with the current state to identify the elements that need updating.

Updated DOM Tree

Here’s where Million.js shines – unlike traditional virtual DOM diffing, which involves recursively traversing each virtual node and comparing it for changes, Million.js performs a shallow equality check on the state.

This optimization makes the diffing process significantly more efficient, as it eliminates the need to traverse the entire virtual DOM tree, leading to faster updates and rendering times.

Supercharged Compiler

In addition to its groundbreaking rendering mechanism, Million.js introduces an experimental compiler that can further optimize React components on the server.

In React, rendering involves recursively traversing the virtual DOM and comparing each node to identify changes. This process occurs during runtime, which means it happens while the code is executed and not during the development phase. Consequently, all these comparisons are made when we execute the code at runtime. If the DOM tree is extensive, these recursive calls can consume a significant amount of time, leading to slower application performance.

Rendering process in React

To overcome these performance challenges, Million.js introduces the experimental compiler, in which the static analysis is carried out during compile time while you are coding rather than during runtime when the application is executed.

Million.js significantly speeds up the rendering process by executing static analysis during compile time. The previously time-consuming comparisons that would have occurred at runtime in React are handled during development, making the execution much faster and more efficient.

Rendering process in Millionjs

Writing Code in Million.js

Now, let’s learn how we can write code in Million.js.

Setting up a Million.js project

You can easily get started with Million.js in an existing React application. Install Million.js in a React app by running:

npm install million

Then, create a config file called craco.config.js in the root directory with the following code:

const million = require('million/compiler');
module.exports = {
 webpack: {
   plugins: { add: [million.webpack()] }
 }
};

Using the block component

Blocks are the fundamental building blocks of Million.js applications. They encapsulate the UI and its behavior. Let’s create a simple component, wrap it inside block, and render that component inside our React App’s <App/> component:

import { block } from "million/react";

const MillionComponent = block(function Component() {
 return <h1>This is a millionjs component!</h1>;
});

function App() {
 return (
   <div>
     <MillionComponent />
   </div>
 );
}

export default App;

The above code should give you the following output:

Output

Rules of using Blocks

While Blocks provide a powerful way to manage UI components, there are some essential rules to follow:

  1. Always import the block from million/react instead of million.

The following import statement for importing block in your code is wrong:

import { block } from 'million'; 

Instead, always import block from the million/react library as shown below:

import { block } from 'million/react'; 
  1. Define blocks as a variable declaration

Whenever you create a block component, assign it to a variable during declaration. For instance, the following code is valid because our block component is declared as a variable called Block:

const Block = block(() => <div>Hello world!</div>) 
export default Block;

However, the following is invalid since there is no variable declaration for block:

export default block(() => <div>Hello world!</div>) 
  1. The blocks should always consume a reference to a component function

In the below code, we’re passing the actual JSX component to our block, which is invalid:

const BlockComponent = block(<Component />) 

When we should be passing the reference to the component function as shown:

const BlockComponent = block(Component)   
  1. Use deterministic returns inside your block components and use native HTML elements

Your block components should always return a single deterministic statement. For instance, the following return statement is incorrect because it’s conditional:

if (age > 18) {
 return <div>You're eligible for a driver's license! </div>;
}

For the same reason, avoid using UI components from libraries inside a block component because they might inherently have undeterministic return statements.

Optimized list rendering using the For component

In scenarios where you need to render large lists of data, Million.js offers an optimized <For/> component.

The <For /> component is used to render a list of blocks. This built-in component takes an array as a prop against the propname each and a function as its children. For each item of the array, the function is called, and that item, along with its index, is passed as arguments.

Here’s a simple example of using the <For/> component to render a simple list to the DOM:

import { block, For } from "million/react";
import { useState } from "react";

function App() {
 const [fruits, setFruits] = useState(["apple", "banana", "mango"]);

 return (
   <>
     <For each={fruits}>{(fruit) => <li>{fruit}</li>}</For>
   </>
 );
}

export default App;

If you run the above code, you should see the rendered list on the DOM as shown below:

Rendered list on the DOM

<For/> automatically handles the list rendering while keeping the DOM updates to a minimum. However, by default, it doesn’t come with the ability to re-use the blocks it renders. In other words, if you use the <For/> component to loop through 100 items and render 100 blocks or components to display that data, it will recreate those 100 blocks every time it renders them.

If you’re sure your items are independent of any other value than the one passed as an argument to the function, you can use the memo prop to make those blocks reusable.

<For memo each={fruits}>{(fruit) => <li>{fruit}</li>}</For>

As shown above, using the memo prop now tells Million to re-use the blocks, which will improve your application’s performance and memory usage.

Let’s take one more example of using <For/>, where it renders some dynamic data. Here, we have a simple input field where you can add an item to a list. This list itself would be rendered using the <For/> component:

import { block, For } from "million/react";
import { useState } from "react";

function App() {
 const [items, setItems] = useState([]);
 const [newItem, setNewItem] = useState("");

 const addItem = () => {
   setItems([newItem, ...items]);
   setNewItem("");
 };

 const handleChange = (e) => setNewItem(e.target.value);
 return (
   <>
     <input
       type="text"
       value={newItem}
       placeholder="Enter an item..."
       onChange={handleChange}
     />
     <button onClick={addItem}>Add Item</button>
     <For each={items}>
       {(item, ind) => (
         <ul key={ind}>
           {ind + 1}. {item}
         </ul>
       )}
     </For>
   </>
 );
}

export default App;

Now, if we add some items to the list, the <For/> component will render them on the DOM:

Rendering dynamic list

Hence, with Million.js, you should avoid using Array.map to render lists and instead use the built-in <For/> component instead. You can learn more about the <For/> component here.

Using macros in Million.js

We’ve talked about how Million.js can execute some code at compile time using its supercharged compiler. Under the hood, it uses a special function called macros.

Any expressions in your code wrapped with macros will run at compile time. Let’s take the following simple example:

import { macro } from 'million/react';

const sumTilln = (n) => {
 if (n == 0) return 0;
 return n + sumTilln(n-1);
};

const value = macro(sumTilln(5)); 
console.log('VALUE: ' + value);   //15

Running the above code should print 15 on the console, as shown:

Macros example

The above code will run at compile time, and the value can be used immediately in your React code. This means the sumTilln function won’t have to run while your React app renders on the browser. Instead, you can just directly use the result of the macro function in your code.

Comparing Million.js’ performance with React

Let’s explore the performance comparison of Million.js against well-established frameworks like React, Svelte, and Solid in more detail to understand the advantages it offers, especially in scenarios involving large-scale data rendering.

The official JavaScript frameworks benchmarking provides valuable insights into the performance of different frameworks by measuring the time taken for various operations. These operations typically include creating 1000 rows of data, updating these rows, highlighting a selected row, swapping two rows in a table, and more.

Operation Benchmarks

When we examine the benchmark results, Million.js outperforms both React and Svelte in all of these operations. Notably, in certain cases, such as swapping two rows in a table with 1000 rows, Million.js demonstrates remarkable performance, up to 5 times more efficient than React. It also competes favorably with Solid in some scenarios.

Geometric Mean Results

When considering the results of various operations across the benchmark, Million.js consistently outperforms React, Svelte, and Solid. The combination of its Block virtual DOM, efficient diffing mechanism, and optimized rendering strategy contributes to these impressive results.

To truly gauge the advantages of Million.js in handling large-scale data rendering, I encourage you to explore this demo and see for yourself how Million.js does a much better job in rendering data up to 1000s of rows than React.

Drawbacks of Million.js

Like every framework ever built, Million.js is not a magic wand for all your performance problems. It’s also not the perfect framework and has its own set of disadvantages.

  1. It’s still relatively new and has a much smaller community, limited documentation, and resources for learning as compared to frameworks like React and Svelte
  2. Million.js requires some learning curve to understand how the Block components work. You could run into bugs and errors during development that might slow you down.
  3. Its performance benefits might not always be evident. In many practical use cases, you won’t run into rendering problems as such. Blindly using it in all of your projects can be an overkill.

Real-World Use Cases

Million.js is well-suited for projects with lots of unchanging content and only a few parts that change frequently. It excels in situations like building admin dashboards or websites with component-heavy layouts where much of the content remains static.

For example, imagine creating an admin dashboard with various charts, tables, and data visualizations. In such a case, Million.js would shine because the layout and design of the dashboard would generally remain fixed, while the data being displayed might update dynamically.

However, if you are working on a project where the computational effort to process the changing data is significantly greater than the effort to update the virtual DOM, Million.js might not demonstrate a substantial advantage. Plain-old React or any other framework of your choice might actually do an equally good job, if not better.

Conclusion

In conclusion, Million.js is a promising framework that revolutionizes front-end development focusing on speed, performance, and memory optimization. However, it’s important to understand where it can be truly useful to use its full potential.

Gain Debugging Superpowers

Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers. Check our GitHub repo and join the thousands of developers in our community.

OpenReplay