Million.js

repository·main·Indexed 12 days ago

https://github.com/aidenybai/million

An optimizing compiler for React that boosts rendering performance by replacing standard reconciliation with a direct DOM update mechanism. It includes a compiler unplugin for Vite, Webpack, Rollup, Rspack, and Esbuild, as well as specialized support for Next.js and React Server Components (RSC). Version 3.1.10.

Tokens
44K
Snippets
169
Records
219
Agent score
92%

What's inside Million.js

  1. Key improvements in Million.js 3.0

    main

    Million 3.0 introduces significant performance optimizations in both the build and runtime phases:

    Faster Build Time

    The compiler has been overhauled for better efficiency and scalability. Benchmarks for a medium-sized Next.js project show:

    • Initial compile: ~34% faster (3.2s -> 2.1s).
    • Code changes: ~82% faster (1.1s -> 0.2s).

    Faster Runtime

    • Hydration: The hydration process has been refactored. Instead of replacing the DOM at component boundaries, Million.js can now pinpoint specific hydration points, resulting in significantly faster hydration times (initial benchmarks show a ~100% improvement, e.g., 2s -> 1s).
    • Reduced DOM overhead: Work is ongoing to remove the dependency on <slot> elements for mounting blocks, which reduces unnecessary DOM nodes, improves reconciliation, and lowers memory usage.
  2. Overview of Million.js Manual Mode

    main
    Manual mode allows developers to selectively opt-in to Million.js optimizations within an existing React application. Instead of applying optimizations globally, you use block() to convert specific components and <For /> to handle list rendering. This granular control allows you to target performance bottlenecks (like large data grids) without changing how the rest of your application works.
  3. Rules for using props in block()

    main

    When using block(), the props object follows specific constraints regarding data types and interpolation:

    1. Allowed Prop Values

    props is an immutable object. It can contain:

    • Primitive values (strings, numbers, booleans, etc.)
    • Other Block instances

    Invalid values:

    • Plain objects (e.g., { imposter: true })
    • Non-primitive objects like new Date()

    2. Hole Interpolation Constraints

    props are filled with immutable Hole objects (e.g., { $: 'prop' }). These are replaced with real values when the block(){:jsx} is called. Because of this, you cannot perform operations or destructuring on these holes.

    Prohibited patterns:

    • Destructuring: const { favorite } = props.favorite; is not allowed.
    • String/Value Interpolation: props.foo + ' bar' is not allowed.
    • Arithmetic/Method calls: props.count + 1 or props.foo.toString() are not allowed.
    • Event Handlers: You cannot access a hole inside an event handler (e.g., onClick={() => console.log(props.world)}).

    Allowed patterns:

    • Direct usage in JSX attributes: <div className={props.className}>
    • Direct usage in JSX children: {props.hello}
    • Using non-hole values: {Date.now()}
    // ✅ Allowed
    block((props) => {
      return <div className={props.className}>
        {props.hello}
        {Date.now()}
      </div>;
    });
    
    // ❌ Prohibited
    block((props) => {
      const { favorite } = props.favorite; // ❌ Destructuring
      return <div className={props.className + ' extra'}>
        {props.count + 1} {/* ❌ Arithmetic */}
        <button onClick={() => console.log(props.world)}> {/* ❌ Hole in event */} 
          Click
        </button>
      </div>;
    });
  4. How `block()` works in Million.js

    main

    The block() function is a Higher-Order Component (HOC) factory used to wrap React components for hyper-optimized rendering via Million.js.

    When you wrap a component with block(), it creates a specialized 'block' that follows a specific lifecycle:

    1. React Rendering: React renders a placeholder component (often called a Loader) which manages the initial lifecycle and state.
    2. DOM Mounting: React mounts this placeholder and attaches a DOM element to a ref.
    3. Million.js Rendering: Million.js takes control of that specific DOM element via the ref and renders the actual component logic using its own high-performance virtual DOM, bypassing React's reconciliation for that specific subtree.

    This allows Million.js to manage a component's DOM independently of React, providing significant performance gains while still allowing the component to be used seamlessly within a React application.

    function MyComponent() {
      // ...
    }
    
    const MyBlock = block(MyComponent);
    
    export default function App() {
      return <MyBlock />;
    }
  5. Understanding Million.js: Beyond Speed to Memory Efficiency

    main

    While Million.js is often marketed as a way to make React "70% faster," its core value proposition extends to improving memory efficiency.

    Standard React applications create large, nested JavaScript objects (the Virtual DOM) for every JSX element. In complex component trees, these objects consume significant RAM and require intensive CPU cycles for "diffing" (comparing old and new trees) during updates.

    Million.js optimizes this process to reduce the memory footprint and CPU overhead, which is particularly beneficial for:

    • Resource-constrained devices: Older laptops, low-end mobile phones, and smart TVs.
    • Long-running applications: Reducing the frequency and impact of Garbage Collection (GC) pauses.
    • Complex UIs: Managing large component trees and frequent state updates without causing sluggishness.
  6. When to avoid Million.js (Highly Dynamic Components)

    main

    Million.js is not suitable for components that are highly dynamic and lack static structure.

    Avoid Million for:

    • Components where data changes extremely frequently (e.g., every second).
    • Components where the rendered structure depends heavily on the data itself (e.g., a list where the component type changes based on the value, like a stock ticker switching between BuyComponent and SellComponent).
    • Components that cannot be analyzed statically by the compiler.

    In these cases, the standard React runtime is better suited to handle the granular updates.

  7. When to use Million.js (Static and Nested Data)

    main

    Million.js excels in several scenarios:

    • Static Components: Blogs, landing pages, and CMS-driven content (e.g., text blocks).
    • CRUD Applications: Forms and pages where data is not constantly shifting.
    • Nested Data: Applications with complex objects containing lists. Million is optimized to traverse these trees efficiently.
    • Optimized Lists: Using the <For /> component instead of Array.prototype.map() to handle dynamic lists efficiently.
  8. How Million.js accelerates React rendering

    main

    Million.js is an optimization compiler that makes React components up to 70% faster by bypassing the standard React reconciliation process.

    The Problem: React Reconciliation

    In standard React, when state changes, React performs 'reconciliation' (diffing). It compares a new snapshot of the component tree against the previous one to identify what changed. This process becomes exponentially slower as the number of JSX elements increases, typically operating at $O(n^3)$ complexity.

    The Solution: Direct DOM Updates

    Million.js transforms reconciliation from $O(n^3)$ to $O(1)$ (constant time). Instead of performing a complex diffing operation across the entire tree, the Million.js compiler generates code that updates specific DOM nodes directly when their dependencies change. This allows components to run at near-native JavaScript speeds.

    // Conceptual example of Million.js generated code
    function App() {
      const [count, setCount] = useState(0);
      const increment = () => setCount(count + 1);
    
      // generated by the compiler
      if (count !== prevCount) {
        <p>.innerHTML = `Count: ${count}`;
      }
    
      <button>.onclick = increment;
    }
  9. How React's Virtual DOM impacts memory and performance

    main

    In standard React, JSX is transpiled into React.createElement() calls, which produce JavaScript objects representing the Virtual DOM.

    The Memory Problem

    1. Object Storage: Every nested element in your JSX becomes a nested JavaScript object. A deep tree results in a massive, complex object structure stored in memory.
    2. Diffing Overhead: When state changes, React must compare the entire old Virtual DOM tree with the new one to determine what changed in the real DOM. This is a CPU-intensive operation.
    3. Garbage Collection (GC) Pressure: Every time a component re-renders, old Virtual DOM objects are discarded and replaced with new ones. This constant cycle of allocation and deallocation forces the browser's Garbage Collector to run frequently. Since GC is a blocking operation, frequent or large-scale collections can introduce noticeable delays (jank) in the UI.

    Best Practices for Standard React

    To mitigate these issues in plain React, it is recommended to move useState() and useEffect() as far down the component tree as possible. Smaller components mean smaller diffing processes and less work for the engine when a specific part of the UI updates.

    // Standard React element representation (simplified)
    {
        $$typeof: Symbol(react.element),
        key: null,
        props: { children: "Hello world" },
        ref: null,
        type: "div"
    }
  10. Best Practices for React Performance: State Placement

    main

    To minimize the cost of the reconciliation (diffing) process, it is a standard React best practice to move useState() and useEffect() as low as possible in the component tree.

    By keeping state localized to the smallest possible component, you ensure that when that state changes, only a small subtree needs to be re-rendered and diffed, rather than a large portion of the application. This reduces both CPU usage and the number of objects that need to be created and subsequently garbage collected.