How Fre's `memo()` HOC Optimizes Component Rendering: A Deep Dive into the Source Code

Fre's memo() higher-order component prevents unnecessary re-renders by marking functional components with a memo flag and using a shallow comparison or custom equality function to skip rendering when props remain unchanged.

Fre is a lightweight React alternative that implements a fiber-based reconciler. Understanding how its memo() higher-order component (HOC) optimizes rendering requires examining the interplay between the component factory in src/h.ts and the reconciliation logic in src/reconcile.ts.

What Is Fre's memo() HOC?

The memo() function is a tiny higher-order component factory located in src/h.ts (lines 51-55). It takes a functional component and an optional comparison function, then attaches metadata to the function object that the reconciler checks during the render phase.

Unlike React's implementation, Fre's version is intentionally minimal, adding only two properties to the component function:

  • fn.memo = true — signals that this component may be skipped
  • fn.shouldUpdate — optionally stores a custom props comparison function

How the Reconciler Detects Memoized Components

The capture() Routine

During each reconciliation cycle, Fre's capture function in src/reconcile.ts traverses the fiber tree. When it encounters a fiber representing a functional component, it checks whether that component has been marked with memo.

The critical logic appears at lines 90-93:

if (isMemo(fiber)) {
  fiber.memo = false
  return sibling(fiber)
}

If isMemo(fiber) returns true, the reconciler immediately returns the next sibling fiber instead of recursing into the component's children. This means the component's render function is not invoked, its child fibers are not diffed, and no DOM work is scheduled for that subtree.

The isMemo() Logic

The isMemo function, defined at lines 104-115 in src/reconcile.ts, performs three validation steps before determining whether to skip rendering:

  1. Component type check — verifies the current and previous fibers refer to the same component type
  2. Props existence check — confirms a previous props object exists for comparison
  3. Equality comparison — executes either the user-provided shouldUpdate function or Fre's default shallow comparison

The default shallow comparison lives at lines 41-47 in src/reconcile.ts:

const shouldUpdate = (a, b) => {
  for (let i in a) if (a[i] !== b[i]) return true
  for (let i in b) if (b[i] !== a[i]) return true
  return false
}

If shouldUpdate returns false (indicating props are equal), isMemo returns true, triggering the skip logic in capture.

Practical Implementation Examples

Using memo() with Custom Comparison

The following example from the Fre demo repository demonstrates using a custom equality function to control when re-renders occur:

import { h, render, useState, memo } from '../../src'

function App() {
  const [count, setCount] = useState(0)
  return (
    <div>
      <Counter count={count} />
      <button onClick={() => setCount(count + 1)}>Inc</button>
    </div>
  )
}

// Re-render only when `count` becomes even
const Counter = memo(
  ({ count }: { count: number }) => <div>{count}</div>,
  (prev, next) => next.count % 2 === 0
)

render(<App />, document.body)

In this implementation, the Counter component skips rendering on every odd update because the custom shouldUpdate returns false for those cases.

Verifying Optimization in Tests

The Fre test suite confirms that memoized components avoid unnecessary render calls:

// test/memo.tsx
import { h, memo } from '../src/index'

export const memor = async t => {
  const Component = memo(() => {
    console.log('rendered')
    return <div />
  })
  // First render
  t.is(await render(<Component />, document.body), undefined)
  // Subsequent render with same props – should not log again
}

This test verifies that when props remain unchanged, the component function is not invoked again, confirming the optimization is working.

Summary

  • Fre's memo() marks functional components by setting fn.memo = true and optionally attaching a custom shouldUpdate comparison function in src/h.ts.
  • The reconciler checks for memoization during the capture phase in src/reconcile.ts, calling isMemo() to determine if props have changed.
  • Shallow comparison is the default equality check, though users can provide custom logic to control when updates occur.
  • Subtree skipping happens immediately when isMemo returns true, preventing the render function from executing and avoiding all child reconciliation and DOM operations for that component.

Frequently Asked Questions

What is the difference between Fre memo() and React memo()?

Both implementations prevent re-renders when props remain unchanged, but Fre's version is significantly more lightweight. While React's implementation involves complex internal state management and additional development-mode checks, Fre simply attaches two properties (memo and shouldUpdate) to the function object and relies on the reconciler's isMemo check during the capture phase to skip rendering.

How does Fre's default shouldUpdate function work?

The default comparison function performs a shallow equality check on props objects. Located at lines 41-47 in src/reconcile.ts, it iterates through all keys in both the previous and current props objects, returning true (indicating an update is needed) if any values differ. If all values match, it returns false, triggering the memoization skip.

Can I use memo() with Fre hooks like useState or useEffect?

Yes, memo() works seamlessly with Fre's hook system. The HOC only controls whether the component function re-executes based on props changes; internal state updates from useState or effects from useEffect still function normally when the component does render. However, if a parent re-renders but the memoized component's props haven't changed, the component won't execute at all, meaning its internal hooks won't run during that specific cycle.

Where does Fre check if a component should update during reconciliation?

The check occurs in the capture function within src/reconcile.ts at lines 90-93. During the reconciliation traversal, when the reconciler encounters a functional component fiber, it calls isMemo(fiber) (defined at lines 104-115). This function verifies the component is marked with memo, compares props using either the custom or default shouldUpdate function, and returns a boolean that determines whether the reconciler should skip the entire subtree.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →