Tutorial
Welcome to the Million.js documentation (woop woop 🎉🤑). Let's learn how you can integrate Million.js into your React applications.
Million.js assumes that you're already familiar with and you're using React. If you're not, is is recommended you check out react.dev (opens in a new tab) first.
You will learn:
- How to use
block()
to convert React components into blocks - How to use
<For />
for efficiently rendering lists - When to use
block()
and<For />
- The limitations of blocks
What's a block?
Million.js is a library that enables you to create blocks. A block is a special Higher Order Component (HOC) (opens in a new tab) that can be used as a React component but is hyper-optimized for rendering speed.
Blocks are essentially components wrapped by block()
.
import { block } from "million/react";
const LionBlock = block(function Lion() {
return (
<img src="https://million.dev/lion.svg" />
);
})
Blocks can be used just like a normal React component:
export default function App() {
return (
<div>
<h1>mil + LION = million</h1>
<LionBlock />
</div>
);
}
Have a look at the result:
Preview
With that in hand, let's build an app.
Data Grid Example
One use case of blocks is rendering lists of data efficiently. In this example, let's build a data grid in React.
You have access to the prebuilt components <Table />
and <Input />
from your fake user interface (UI) library. You can then store the number of rows you want to display in a useState()
hook.
function App() {
const [rows, setRows] = useState(1);
return (
<div>
<Input value={rows} setValue={setRows} />
<Table>
// ...
</Table>
</div>
);
}
But wait! You made a grid but you have no data. Let's say you can grab some array of arbitrary data from a function called buildData(rows)
:
const data = buildData(100);
// returns [{ adjective: '...', color: '...', noun: '...' }, ... x100]
Now, you can render the data in our table using Array.map()
:
function App() {
const [rows, setRows] = useState(1);
const data = buildData(rows);
return (
<div>
<Input value={rows} setValue={setRows} />
<Table>
{data.map(({ adjective, color, noun }) => (
<tr>
<td>{adjective}</td>
<td>{color}</td>
<td>{noun}</td>
</tr>
))}
</Table>
</div>
);
}
Preview
You can see that it performs pretty well. From 0-100, there's virtually no lag, but once you get higher than 500 or so, there's a noticable delay in rendering.
React is great because you can declaratively write great UI and get pretty good performance. But the data grid you just made is a rudimentary example, and is not necessarily representative of most React applications.
More realistic rendering
In the following example, you add lotsOfElements
(an array of a lot of blank elements) to each row. you can also add a lag radar to monitor page performance.
Try changing the input value up and down from 0 to 1000. Notice how React really struggles when rendering a lot of elements.
Editable example
import { useState } from 'react'; import { Table, Input, lotsOfElements } from './ui'; import { buildData } from './data'; function App() { const [rows, setRows] = useState(1); const data = buildData(rows); return ( <div> <Input value={rows} setValue={setRows} /> <Table showRadar> {data.map(({ adjective, color, noun }) => ( <tr> <td>{adjective}</td> <td>{color}</td> <td>{noun}</td> <td>{...lotsOfElements}</td> </tr> ))} </Table> </div> ); } export default App;
Preview
Just block
it
In the following example, you can use block()
and <For />
in order to optimize rendering.
First, you need to abstract the <tr>
into its own component.
data.map(({ adjective, color, noun }) => (
<tr>
<td>{adjective}</td>
<td>{color}</td>
<td>{noun}</td>
<td>{...lotsOfElements}</td>
</tr>
))
// 👇👇👇
function Row({ adjective, color, noun }) {
return (
<tr>
<td>{adjective}</td>
<td>{color}</td>
<td>{noun}</td>
{...lotsOfElements}
</tr>
);
}
Then, you can wrap it with block()
in order to optimize the <Row />
component.
import { block } from "million/react";
const RowBlock = block(
function Row({ adjective, color, noun }) {
return (
<tr>
<td>{adjective}</td>
<td>{color}</td>
<td>{noun}</td>
{...lotsOfElements}
</tr>
);
}
);
Once, you've optimized a row, you will need to render it as a list:
data.map(({ adjective, color, noun }) => (
<RowBlock adjective={adjective} color={color} noun={noun}>
));
BUT WAIT! You can actually use Million.js' built-in rendering solution.
Optimized List Rendering
The <For />
component is used to render a list of blocks. It takes an array as the each
prop and a function as its children. The function is called for each item in the array and is passed the item and its index as arguments.
<For />
Component
Syntax: <For each={array}>{(item, index) => Block}</For>
Example: <For each={[1, 2, 3]}>{(item, index) => myBlock({ item, index })}</For>
It's the best way to loop over an array (uses mapArray()
under the hood). As the array changes, <For />
updates or moves items in the DOM rather than recreating them. Let's look at an example:
With this in mind, you can rewrite your table to use <For />
:
import { For } from "million/react";
<For each={data}>
{({ adjective, color, noun }) => (
<RowBlock adjective={adjective} color={color} noun={noun} />
)}
</For>
Now that we've integrated Million.js, let's check the new example out.
Notice when you change the input value, the lag radar shows significantly less lag than the pure React example. With a faster underlying virtual DOM, Million.js can take a lot of the pain out of rendering large lists.
Editable example
import { useState } from 'react'; import { Table, Input, lotsOfElements } from './ui'; import { buildData } from './data'; import { block, For } from 'million/react'; const RowBlock = block( function Row({ adjective, color, noun }) { return ( <tr> <td>{adjective}</td> <td>{color}</td> <td>{noun}</td> {...lotsOfElements} </tr> ); } ); function App() { const [rows, setRows] = useState(1); const data = buildData(rows); return ( <div> <Input value={rows} setValue={setRows} /> <Table showRadar> <For each={data}> {({ adjective, color, noun }) => ( <RowBlock adjective={adjective} color={color} noun={noun} /> )} </For> </Table> </div> ); } export default App;
Preview
Million.js vs. React
The following is a more comprehensive demo using key-based rendering (opens in a new tab) to show how Million.js performance compares to React.
Hitting the limit
This section is a bit more advanced. If you want a list of limitations, check out the Rules of Blocks. Or, if you just want to start integrating Million.js, check out the installation guide.
Blocks are great for rendering large lists, data grids, and many other use cases. Under the hood, they render with the Million.js virtual DOM instead of React.
Deep Dive: How does it work?
(1/6)
Using a block can allow us to capture potential performance gains. However, you should always use best judgement, as blocks are not a silver bullet. Here are some general guidelines to follow:
- Static views: Blocks perform best when there's not that much dynamic data. Since static parts of the React tree need to be unnecessary rerendered when dynamic data changes by React, blocks can directly skip to what's dynamic.
- Nested data: Blocks are great for rendering nested data. Million.js turns tree traversal from
O(tree)
toO(1)
, allowing for fast access and changes.
Looking for the full guidelines? Check out Rules of Blocks.
Next Steps
By now, you know the basics of how to integrate Million.js into your application!
Check out the installation guide to put them into practice and start using blocks.
This page is directly inspired by React's "Quick Start" page (opens in a new tab).