MinhVo

Minh Vo

rss feed

Slaying code & making it lit fr fr 🔥 tagline

Hey there 👋 I'm an AI Engineer with 7 years of experience building scalable web and mobile applications. Currently at Neurond AI (May 2025 — present), architecting an Enterprise AI Assistant Platform with multi-tenant RAG on pgvector, multi-provider LLM orchestration, and Azure-native infrastructure. Previously spent 5+ years at SNAPTEC (Sep 2019 — Apr 2025), leading SaaS themes, admin dashboards, and e-commerce platforms — earned the Hero of the Year award in 2021. I specialize in TypeScript, React, Next.js, and AI-Native engineering with Claude Code and Cursor.bio

Back to blogs

React Native Fabric: The New Rendering System

Understand Fabric: synchronous rendering, concurrent features, and improved performance.

React NativeFabricRenderingMobile

By MinhVo

Introduction

Fabric is React Native's new rendering system that fundamentally reimagines how JavaScript components become native UI elements on screen. Where the old architecture relied on asynchronous message passing through a serialized bridge — creating latency in scroll-heavy interfaces, complex animations, and rapid state updates — Fabric introduces a synchronous, multi-threaded rendering pipeline that brings React Native's rendering performance on par with native applications.

This isn't a minor optimization. Fabric changes the core communication model between JavaScript and native platforms, enabling features like React Suspense, concurrent rendering, and true 60fps interactions that were impossible under the old architecture. Understanding how Fabric works is essential for any React Native developer building performance-critical applications in 2024 and beyond.

Rendering Pipeline Architecture

Understanding Fabric: Core Concepts and Architecture

The Old Architecture's Limitations

To understand why Fabric exists, we need to understand what it replaces. The original React Native architecture used a three-layer bridge model:

  1. JavaScript Thread: Executes your React components and business logic
  2. Bridge: Serialized JSON messages passed asynchronously between JS and native
  3. Native Thread: Runs native UI components (UIKit on iOS, Android Views)

The bridge was a bottleneck. When JavaScript wanted to update a view property, it serialized the change as JSON, sent it across the bridge, and the native side deserialized it and applied the update. This round-trip introduced at least one frame of latency (16.67ms at 60fps) and could accumulate during complex updates.

For simple UIs, this latency was imperceptible. For gesture-driven interfaces, scroll-linked animations, or rapid state updates, it created visible jank and unresponsive interactions.

The Fabric Rendering Pipeline

Fabric eliminates the asynchronous bridge for rendering operations by using the JavaScript Interface (JSI) — a lightweight, synchronous C++ layer that lets JavaScript directly invoke native functions without serialization. The rendering pipeline works in four phases:

Render Phase (JavaScript): React components produce a virtual tree of "shadow nodes" — lightweight C++ objects that describe layout properties. This happens synchronously on the JS thread.

Commit Phase (C++): The new shadow tree is diffed against the current tree. Only the changed nodes are identified and prepared for mounting. This comparison happens on a background thread without blocking JS.

Mount Phase (Native): The diff is applied to the actual native view hierarchy on the main UI thread. Only changed views are updated, minimizing main-thread work.

Layout Phase (Yoga): The Yoga layout engine computes positions and sizes synchronously within the render phase, eliminating the need for an additional layout pass on the native side.

Thread Model

Fabric distributes work across multiple threads:

  • JavaScript Thread: Runs React reconciliation and produces shadow trees
  • Background Thread: Performs tree diffing and layout computation
  • Main Thread (UI): Mounts view changes and handles user interactions

The key innovation is that the background thread can prepare the next frame while the main thread is rendering the current one. This parallelism eliminates the one-frame latency that plagued the old architecture.

Multi-threaded Rendering

Architecture and Component System

Shadow Tree Architecture

In Fabric, every React component produces a "shadow node" — a C++ object that stores layout information. Shadow nodes form a tree that mirrors the component tree but lives in C++ memory, accessible from both JavaScript and native code without serialization.

// When React renders this component:
function Card({ title, body }: CardProps) {
  return (
    <View style={styles.card}>
      <Text style={styles.title}>{title}</Text>
      <Text style={styles.body}>{body}</Text>
    </View>
  );
}
 
// Fabric creates shadow nodes in C++:
// ShadowNode("View", layout: { width: flex, padding: 16 })
//   └── ShadowNode("Text", layout: { fontSize: 18, fontWeight: bold })
//   └── ShadowNode("Text", layout: { fontSize: 14, color: gray })

Event Handling in Fabric

User events like touches, scrolls, and gestures flow from native to JavaScript through the same JSI layer, enabling synchronous event handling:

import { Gesture, GestureDetector } from 'react-native-gesture-handler';
import Animated, {
  useSharedValue,
  useAnimatedStyle,
  withSpring,
} from 'react-native-reanimated';
 
export function DraggableCard() {
  const translateX = useSharedValue(0);
  const translateY = useSharedValue(0);
 
  const gesture = Gesture.Pan()
    .onUpdate((event) => {
      // In Fabric, this update is synchronous with the gesture
      translateX.value = event.translationX;
      translateY.value = event.translationY;
    })
    .onEnd(() => {
      translateX.value = withSpring(0);
      translateY.value = withSpring(0);
    });
 
  const animatedStyle = useAnimatedStyle(() => ({
    transform: [
      { translateX: translateX.value },
      { translateY: translateY.value },
    ],
  }));
 
  return (
    <GestureDetector gesture={gesture}>
      <Animated.View style={[styles.card, animatedStyle]}>
        <Text>Drag me</Text>
      </Animated.View>
    </GestureDetector>
  );
}

Concurrent Rendering Features

Fabric enables React 18's concurrent features in React Native:

import { Suspense, useTransition } from 'react';
import { ActivityIndicator } from 'react-native';
 
export function ProductScreen() {
  const [isPending, startTransition] = useTransition();
  const [filter, setFilter] = useState('all');
 
  const handleFilterChange = (newFilter: string) => {
    startTransition(() => {
      // This state update has lower priority
      // Fabric can interrupt it for urgent updates (like gestures)
      setFilter(newFilter);
    });
  };
 
  return (
    <View>
      <FilterBar onChange={handleFilterChange} />
      {isPending && <ActivityIndicator />}
      <Suspense fallback={<ProductSkeleton />}>
        <ProductList filter={filter} />
      </Suspense>
    </View>
  );
}

Step-by-Step Implementation

Enabling Fabric in Your Project

Fabric is opt-in. Enable it through your native project configuration:

For iOS (in Podfile):

# Set the new architecture flag
ENV['RCT_NEW_ARCH_ENABLED'] = '1'
 
# Install pods with the new architecture
pod install

For Android (in gradle.properties):

newArchEnabled=true

Using Expo (in app.json):

{
  "expo": {
    "plugins": [
      [
        "expo-build-properties",
        {
          "ios": { "newArchEnabled": true },
          "android": { "newArchEnabled": true }
        }
      ]
    ]
  }
}

Creating Fabric-Compatible Native Components

Native components need to implement the Fabric component interface:

// MyComponentNativeComponent.ts (codegen spec)
import type { ViewProps } from 'react-native';
import type { HostComponent } from 'react-native';
import codegenNativeComponent from 'react-native/Libraries/Utilities/codegenNativeComponent';
 
interface MyComponentProps extends ViewProps {
  color: string;
  size: number;
  onChange?: (event: { value: string }) => void;
}
 
export default codegenNativeComponent<MyComponentProps>(
  'MyComponent'
) as HostComponent<MyComponentProps>;

Measuring Fabric Performance

Compare rendering performance between old and new architectures:

import { PerformanceObserver } from 'react-native-performance';
 
export function useRenderMetrics(componentName: string) {
  const renderStart = useRef(Date.now());
 
  useEffect(() => {
    const renderTime = Date.now() - renderStart.current;
    if (renderTime > 16) {
      console.warn(
        `[${componentName}] Slow render: ${renderTime}ms ` +
        `(budget: 16ms for 60fps)`
      );
    }
  });
 
  renderStart.current = Date.now();
}

Performance Comparison

Real-World Use Cases

Use Case 1: 60fps Scroll-Linked Animations

Under the old architecture, animating elements based on scroll position caused visible jank because scroll events crossed the asynchronous bridge. Fabric eliminates this latency:

import Animated, {
  useAnimatedScrollHandler,
  useAnimatedStyle,
  interpolate,
  Extrapolation,
} from 'react-native-reanimated';
 
export function ParallaxHeader() {
  const scrollY = useSharedValue(0);
 
  const scrollHandler = useAnimatedScrollHandler({
    onScroll: (event) => {
      scrollY.value = event.contentOffset.y;
    },
  });
 
  const headerStyle = useAnimatedStyle(() => ({
    transform: [
      {
        translateY: interpolate(
          scrollY.value,
          [0, 200],
          [0, -100],
          Extrapolation.CLAMP
        ),
      },
    ],
    opacity: interpolate(
      scrollY.value,
      [0, 200],
      [1, 0],
      Extrapolation.CLAMP
    ),
  }));
 
  return (
    <View style={styles.container}>
      <Animated.ScrollView onScroll={scrollHandler} scrollEventThrottle={16}>
        <Animated.View style={[styles.header, headerStyle]}>
          <Text style={styles.title}>Parallax Header</Text>
        </Animated.View>
        <Content />
      </Animated.ScrollView>
    </View>
  );
}

Use Case 2: Gesture-Driven UI with No Perceptible Latency

Fabric's synchronous event delivery enables gesture interactions that feel identical to native:

const gesture = Gesture.Pan()
  .onBegin(() => {
    scale.value = withTiming(1.05, { duration: 100 });
  })
  .onUpdate((event) => {
    // Fabric delivers these updates synchronously — no frame delay
    translateX.value = event.translationX;
    translateY.value = event.translationY;
  })
  .onEnd((event) => {
    if (Math.abs(event.velocityX) > 500) {
      translateX.value = withTiming(
        event.velocityX > 0 ? SCREEN_WIDTH : -SCREEN_WIDTH,
        { duration: 200 }
      );
      runOnJS(onDismiss)();
    } else {
      translateX.value = withSpring(0);
      translateY.value = withSpring(0);
    }
    scale.value = withTiming(1, { duration: 100 });
  });

Use Case 3: React Suspense for Data Loading

Fabric enables Suspense boundaries that gracefully handle loading states:

import { Suspense, lazy } from 'react';
import { View, ActivityIndicator, StyleSheet } from 'react-native';
 
const ProductDetail = lazy(() => import('./ProductDetail'));
const Reviews = lazy(() => import('./Reviews'));
 
export function ProductScreen({ productId }: { productId: string }) {
  return (
    <View style={styles.container}>
      <Suspense fallback={<SkeletonLoader />}>
        <ProductDetail id={productId} />
      </Suspense>
      <Suspense fallback={<ReviewsSkeleton />}>
        <Reviews productId={productId} />
      </Suspense>
    </View>
  );
}
 
const styles = StyleSheet.create({
  container: { flex: 1, backgroundColor: '#fff' },
});

Best Practices for Production

  1. Test with both architectures during migration: Run your test suite with Fabric enabled and disabled to catch behavioral differences. Use environment flags to switch between architectures.

  2. Optimize shadow tree depth: Fabric performs better with flatter component trees. Use View flattening and avoid unnecessary wrapper components that increase tree depth.

  3. Leverage concurrent features for priority management: Use startTransition to mark non-urgent state updates so Fabric can prioritize gesture responses and animations.

  4. Profile before and after enabling Fabric: Use React DevTools Profiler and native performance tools to measure actual improvements. Some apps see dramatic gains; others see marginal improvement.

  5. Update third-party native modules: Many community libraries have Fabric-compatible versions. Run npx react-native-community/cli doctor to check compatibility.

  6. Use useAnimatedStyle for all animated properties: Fabric's synchronous rendering makes useAnimatedStyle significantly more powerful — it runs on the UI thread without crossing the bridge.

  7. Implement proper error boundaries: Concurrent rendering can surface errors differently. Add error boundaries at strategic points in your component tree.

  8. Monitor memory usage: Fabric's shadow tree uses C++ memory. Complex apps with deep component trees may see slightly higher memory usage, offset by the elimination of JSON serialization buffers.

Common Pitfalls and Solutions

PitfallImpactSolution
Third-party library crashes with Fabric enabledApp crash on startupCheck library's GitHub for Fabric support; many have specific migration branches
Increased memory usage from shadow treeOOM on low-memory devicesFlatten component hierarchy; remove unnecessary nested Views
Event handler called twice during migrationDuplicate side effectsGuard event handlers with useRef flags; check if this is a known Fabric timing issue
Layout computation differences between architecturesUI shifts or misalignment after enabling FabricCompare Yoga output in both modes; adjust styles if needed
Concurrent mode rendering unexpected intermediate statesUI shows stale or incomplete dataUse useTransition to control when updates commit; implement proper loading states
Native module not receiving events in FabricFeatures silently breakMigrate native modules to use JSI directly instead of the legacy bridge

Performance Optimization

Measuring Rendering Performance

import { InteractionManager } from 'react-native';
 
export function measureFrameRate(duration: number = 5000): Promise<number> {
  return new Promise((resolve) => {
    let frames = 0;
    const startTime = performance.now();
 
    function tick() {
      frames++;
      if (performance.now() - startTime < duration) {
        requestAnimationFrame(tick);
      } else {
        const elapsed = performance.now() - startTime;
        resolve((frames / elapsed) * 1000);
      }
    }
 
    InteractionManager.runAfterInteractions(() => {
      requestAnimationFrame(tick);
    });
  });
}
 
// Typical results:
// Old Architecture (bridge): 45-55 fps during scroll
// Fabric: 58-62 fps during scroll

Optimizing Component Rendering with Fabric

import { memo, useMemo } from 'react';
 
// Memoize expensive list items — Fabric + memo = minimal re-renders
const ProductCard = memo(({ product }: { product: Product }) => {
  const formattedPrice = useMemo(
    () => new Intl.NumberFormat('en-US', {
      style: 'currency',
      currency: 'USD',
    }).format(product.price),
    [product.price]
  );
 
  return (
    <View style={styles.card}>
      <Image source={{ uri: product.image }} style={styles.image} />
      <Text style={styles.name}>{product.name}</Text>
      <Text style={styles.price}>{formattedPrice}</Text>
    </View>
  );
});
 
ProductCard.displayName = 'ProductCard';

Comparison with Alternatives

FeatureFabricOld ArchitectureFlutter RenderingJetpack Compose
CommunicationSynchronous (JSI)Asynchronous (Bridge)Direct (Dart VM)Direct (Kotlin)
Threading ModelMulti-threadedSingle main threadMulti-threadedSingle thread + coroutines
Concurrent RenderingYes (React 18)NoYes (Impeller)Yes (Compose runtime)
Gesture Latency<1 frame1-3 frames<1 frame<1 frame
Memory ModelC++ shadow treeJSON buffersSkia renderingCompose nodes
Migration EffortModerateN/AN/AN/A

Testing Strategies

Test Fabric-specific behavior with dedicated test cases:

import { render, act } from '@testing-library/react-native';
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
 
describe('Fabric Rendering', () => {
  it('should render synchronously with Fabric enabled', async () => {
    const startTime = performance.now();
 
    await act(async () => {
      render(<ComplexList items={MOCK_DATA} />);
    });
 
    const renderTime = performance.now() - startTime;
    expect(renderTime).toBeLessThan(100); // Should render in under 100ms
  });
 
  it('should handle rapid state updates without jank', async () => {
    const { getByTestId } = render(<Counter />);
 
    // Simulate rapid button presses
    for (let i = 0; i < 50; i++) {
      await act(async () => {
        fireEvent.press(getByTestId('increment'));
      });
    }
 
    expect(getByTestId('count')).toHaveTextContent('50');
  });
});

Future Outlook

Fabric is the foundation for several upcoming React Native features. React Server Components for React Native are being developed on top of Fabric's synchronous rendering model. The Static Hermes AOT compiler will leverage Fabric's C++ shadow tree for even faster rendering. The vision is a rendering system where JavaScript can drive native UI at the same performance level as SwiftUI or Jetpack Compose while maintaining React's declarative programming model.

Fabric Migration Strategy

Migrate React Native components from the old architecture to Fabric incrementally. Start by identifying native components that use the legacy UIManager API. Rewrite these components using the Fabric component interface, which uses C++ shadow nodes instead of Android/Java and iOS/Objective-C views for layout calculation. Test migrated components thoroughly on both platforms, as Fabric changes the threading model for layout and rendering. Use the React Native Upgrade Helper to track compatibility between React Native versions and Fabric support.

Production Deployment and Monitoring

Deploying React applications to production requires careful consideration of build optimization, error tracking, and performance monitoring. A well-configured production build can significantly improve user experience through faster load times and more reliable error reporting.

Build Optimization Checklist

Before deploying, verify that your production build is fully optimized:

// next.config.js
module.exports = {
  reactStrictMode: true,
  poweredByHeader: false,
  compress: true,
 
  // Optimize images
  images: {
    formats: ['image/avif', 'image/webp'],
    deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048],
    minimumCacheTTL: 60 * 60 * 24 * 30, // 30 days
  },
 
  // Security headers
  async headers() {
    return [{
      source: '/(.*)',
      headers: [
        { key: 'X-Frame-Options', value: 'DENY' },
        { key: 'X-Content-Type-Options', value: 'nosniff' },
        { key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' },
      ],
    }];
  },
 
  // Webpack optimization
  webpack: (config, { isServer }) => {
    if (!isServer) {
      config.optimization.splitChunks = {
        chunks: 'all',
        cacheGroups: {
          vendor: {
            test: /[\\/]node_modules[\\/]/,
            name: 'vendor',
            chunks: 'all',
          },
        },
      };
    }
    return config;
  },
};

Error Tracking Integration

Configure Sentry or a similar error tracking service to capture and categorize production errors:

import * as Sentry from '@sentry/nextjs';
 
Sentry.init({
  dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
  tracesSampleRate: 0.1,
  replaysSessionSampleRate: 0.1,
  replaysOnErrorSampleRate: 1.0,
  integrations: [
    new Sentry.BrowserTracing(),
    new Sentry.Replay({
      maskAllText: true,
      blockAllMedia: true,
    }),
  ],
  beforeSend(event) {
    // Filter out known non-critical errors
    if (event.exception?.values?.[0]?.type === 'ChunkLoadError') {
      return null;
    }
    return event;
  },
});

Health Check Endpoints

Implement health check endpoints that your load balancer and monitoring systems can use to verify application availability:

// pages/api/health.ts
export default async function handler(req, res) {
  try {
    // Check database connectivity
    await db.raw('SELECT 1');
 
    // Check external service dependencies
    const redisPing = await redis.ping();
 
    res.status(200).json({
      status: 'healthy',
      timestamp: new Date().toISOString(),
      services: {
        database: 'connected',
        redis: redisPing === 'PONG' ? 'connected' : 'degraded',
      },
      uptime: process.uptime(),
    });
  } catch (error) {
    res.status(503).json({
      status: 'unhealthy',
      error: error.message,
    });
  }
}

This comprehensive monitoring approach ensures you detect and respond to production issues quickly, maintaining high availability for your users.

Community Resources and Further Learning

The technology landscape evolves rapidly, making continuous learning essential for maintaining expertise. Building a systematic approach to staying current with developments in your technology stack ensures you can leverage new features and avoid deprecated patterns.

Curated Learning Pathways

Rather than consuming content randomly, create structured learning pathways aligned with your current projects and career goals. Start with official documentation and specification documents, which provide the most accurate and comprehensive information. Follow this with hands-on tutorials and workshops that reinforce concepts through practical application.

Technical blogs from framework maintainers and core team members often provide deeper insights into design decisions and upcoming features. Subscribe to the official blogs of your primary frameworks and libraries to stay ahead of breaking changes and deprecation timelines.

Contributing to Open Source

Contributing to open-source projects in your technology stack provides unparalleled learning opportunities. Start with documentation improvements and bug reports, then progress to fixing small issues tagged as "good first issue" in your favorite projects. This direct engagement with maintainers and the codebase accelerates your understanding far beyond what passive learning can achieve.

# Setting up for contribution
git clone https://github.com/project/repository.git
cd repository
git checkout -b fix/issue-description
 
# Run the project's contribution setup
npm run setup:dev
npm run test  # Ensure tests pass before making changes
 
# Make your changes, then run the full test suite
npm run test:full
npm run lint
npm run build
 
# Submit your contribution
git add -A
git commit -m "fix: description of the fix
 
Closes #1234"
git push origin fix/issue-description

Building a Technical Knowledge Base

Maintain a personal knowledge base that captures insights, solutions, and patterns you discover during your work. Tools like Obsidian, Notion, or even a simple Markdown repository can serve as an external memory that grows more valuable over time.

Organize your notes by topic rather than chronologically, and include code examples, links to relevant documentation, and explanations of why certain approaches work better than others. When you encounter a particularly insightful article or conference talk, write a summary that captures the key takeaways and how they apply to your current projects.

Follow key conferences and their published talks to stay informed about emerging patterns and best practices. Many conferences publish recorded talks on YouTube within weeks of the event, making world-class technical content freely accessible.

Join relevant Discord servers, Slack communities, and forums where practitioners discuss real-world challenges and solutions. These communities provide early warning about emerging issues and access to collective wisdom that isn't available through formal documentation.

Mentorship and Knowledge Sharing

Teaching others is one of the most effective ways to deepen your own understanding. Consider writing technical blog posts, giving talks at local meetups, or mentoring junior developers. The process of explaining concepts to others forces you to organize your knowledge and identify gaps in your understanding.

Pair programming sessions with colleagues of different experience levels create mutual learning opportunities. Senior developers gain fresh perspectives on problems they've solved the same way for years, while junior developers benefit from exposure to production-grade thinking and decision-making processes.

Conclusion

Fabric represents the most significant architectural change in React Native's history. By replacing the asynchronous bridge with synchronous JSI communication and introducing concurrent rendering capabilities, it eliminates the performance gap that has been React Native's primary criticism.

Key takeaways:

  1. Synchronous rendering eliminates bridge latency — gestures and animations are frame-perfect
  2. Multi-threaded pipeline maximizes CPU utilization — layout on background, mount on main
  3. Concurrent rendering enables priority-based updates — gestures always take precedence over data updates
  4. JSI replaces JSON serialization — direct C++ function calls instead of serialized messages
  5. React 18 features become available — Suspense, transitions, and streaming SSR
  6. Migration is incremental — enable Fabric alongside existing code and migrate components individually
  7. Performance improvements are measurable — 15-30% rendering performance gains in typical applications

Enable Fabric in your next project to experience truly native-feeling interactions powered by React's declarative model.