Skip to content

Turn off @babel/plugin-transform-destructuring plugin - #58965

Open
retyui wants to merge 1 commit into
react:mainfrom
retyui:patch-2
Open

retyui wants to merge 1 commit into
react:mainfrom
retyui:patch-2

Conversation

@retyui

@retyui retyui commented Oct 9, 2026

Copy link
Copy Markdown
Contributor

Summary:

The latest Hermes engine work well with destructuring syntax. I decided to checked perf. on RN 0.88.x + Samsung S10 and based on the result I don't see the reason to keep transforming destructuring syntax:

Screenshot 2026-10-09 at 12 07 16 Screenshot 2026-10-09 at 12 07 33 Screenshot 2026-10-09 at 12 07 49

Changelog:

[GENERAL] [BREAKING] - Turn off @babel/plugin-transform-destructuring plugin

Test Plan:

  1. Create a RN 0.88.x
  2. Update babel.config.js to toggle unstable_preserveDestructuring: true/false
  3. Apply the next code:
// App.tsx
import React, {useCallback, useState} from 'react';
import {Pressable, ScrollView, StatusBar, StyleSheet, Text, View} from 'react-native';
import {SafeAreaProvider, SafeAreaView} from 'react-native-safe-area-context';

import {BENCH_NAMES, getSink, runBench, type BenchResult} from './destructuringBench';

const ITERATION_OPTIONS = [100_000, 1_000_000, 10_000_000];

function ResultRow({result}: {result: BenchResult}) {
  return (
    <View style={styles.card}>
      <Text style={styles.cardTitle}>{result.name}</Text>
      <Text style={styles.cardMeta}>
        {result.ms.toFixed(2)} ms · {Math.round(result.opsPerMs).toLocaleString()} ops/ms
      </Text>
    </View>
  );
}

function App() {
  const [iterations, setIterations] = useState(ITERATION_OPTIONS[1]);
  const [results, setResults] = useState<BenchResult[]>([]);
  const [running, setRunning] = useState(false);
  const [totalMs, setTotalMs] = useState<number | null>(null);

  const runAll = useCallback(() => {
    setRunning(true);
    setResults([]);
    setTotalMs(null);

    const collected: BenchResult[] = [];
    const runNext = (index: number) => {
      if (index >= BENCH_NAMES.length) {
        const total = collected.reduce((sum, r) => sum + r.ms, 0);
        console.log(
          `[destructuring-bench] iterations=${iterations} total=${total.toFixed(2)}ms sink=${getSink()}`,
        );
        console.log(
          collected.map(r => `${r.ms.toFixed(2).padStart(10)} ms  ${r.name}`).join('\n'),
        );
        setTotalMs(total);
        setRunning(false);
        return;
      }
      collected.push(runBench(index, iterations));
      setResults([...collected]);
      // Yield to the UI between benches so progress is visible.
      setTimeout(() => runNext(index + 1), 0);
    };
    setTimeout(() => runNext(0), 50);
  }, [iterations]);

  return (
    <SafeAreaProvider>
      <StatusBar barStyle="light-content" />
      <SafeAreaView style={styles.safeArea}>
        <ScrollView contentInsetAdjustmentBehavior="automatic" contentContainerStyle={styles.content}>
          <Text style={styles.title}>Hermes destructuring bench</Text>
          <Text style={styles.subtitle}>
            {BENCH_NAMES.length} cases · {iterations.toLocaleString()} iterations each
          </Text>

          <View style={styles.row}>
            {ITERATION_OPTIONS.map(option => (
              <Pressable
                key={option}
                disabled={running}
                onPress={() => setIterations(option)}
                style={[styles.chip, option === iterations && styles.chipActive]}>
                <Text style={styles.chipText}>{option.toLocaleString()}</Text>
              </Pressable>
            ))}
          </View>

          <Pressable
            disabled={running}
            onPress={runAll}
            style={[styles.button, running && styles.buttonDisabled]}>
            <Text style={styles.buttonText}>{running ? 'Running…' : 'Run all'}</Text>
          </Pressable>

          {totalMs !== null && <Text style={styles.total}>Total: {totalMs.toFixed(2)} ms</Text>}

          {results.map(result => (
            <ResultRow key={result.name} result={result} />
          ))}
        </ScrollView>
      </SafeAreaView>
    </SafeAreaProvider>
  );
}

const styles = StyleSheet.create({
  safeArea: {
    flex: 1,
    backgroundColor: '#0f172a',
  },
  content: {
    paddingHorizontal: 16,
    paddingTop: 16,
    paddingBottom: 16,
  },
  title: {
    color: '#e2e8f0',
    fontSize: 24,
    fontWeight: '700',
    marginBottom: 6,
  },
  subtitle: {
    color: '#94a3b8',
    fontSize: 14,
    marginBottom: 16,
  },
  row: {
    flexDirection: 'row',
    gap: 8,
    marginBottom: 12,
  },
  chip: {
    paddingHorizontal: 12,
    paddingVertical: 6,
    borderRadius: 999,
    backgroundColor: '#1e293b',
  },
  chipActive: {
    backgroundColor: '#2563eb',
  },
  chipText: {
    color: '#e2e8f0',
    fontSize: 13,
  },
  button: {
    backgroundColor: '#2563eb',
    borderRadius: 10,
    paddingVertical: 12,
    alignItems: 'center',
    marginBottom: 16,
  },
  buttonDisabled: {
    opacity: 0.5,
  },
  buttonText: {
    color: '#ffffff',
    fontSize: 16,
    fontWeight: '700',
  },
  total: {
    color: '#facc15',
    fontSize: 16,
    fontWeight: '700',
    marginBottom: 12,
  },
  card: {
    borderRadius: 14,
    marginBottom: 14,
    padding: 12,
    backgroundColor: '#111827',
    borderWidth: 1,
    borderColor: 'rgba(255,255,255,0.08)',
  },
  cardTitle: {
    color: '#ffffff',
    fontSize: 16,
    fontWeight: '700',
    marginBottom: 4,
  },
  cardMeta: {
    color: '#cbd5e1',
    fontSize: 12,
  },
});

export default App;


// destructuringBench
// Micro-benchmarks for destructuring in Hermes.
// Every case writes into `sink` so the engine can't drop the work as dead code.

export type BenchResult = {
  name: string;
  iterations: number;
  ms: number;
  opsPerMs: number;
};

type Bench = {
  name: string;
  run: (iterations: number) => number;
};

declare const performance: {now(): number} | undefined;

const now: () => number =
  typeof performance !== 'undefined' && performance.now
    ? () => performance.now()
    : () => Date.now();

const POOL_SIZE = 1024;
const MASK = POOL_SIZE - 1;

const objPool: Array<{x: number; y: number; z: number; w: number}> = [];
const nestedPool: Array<{pos: {x: number; y: number}; meta: {id: number}}> = [];
const arrPool: number[][] = [];
const shortArrPool: number[][] = [];

for (let i = 0; i < POOL_SIZE; i++) {
  objPool.push({x: i, y: i * 2, z: i * 3, w: i * 4});
  nestedPool.push({pos: {x: i, y: i + 1}, meta: {id: i}});
  arrPool.push([i, i + 1, i + 2, i + 3, i + 4, i + 5, i + 6, i + 7]);
  shortArrPool.push(i % 2 === 0 ? [i] : [i, i + 1]);
}

function sumParams({x, y}: {x: number; y: number}) {
  return x + y;
}

function sumArrayParams([a, b]: number[]) {
  return a + b;
}

const benches: Bench[] = [
  {
    name: 'let {x, y} = obj',
    run: n => {
      let sink = 0;
      for (let i = 0; i < n; i++) {
        let {x, y} = objPool[i & MASK];
        sink += x + y;
      }
      return sink;
    },
  },
  {
    name: 'let {x, y, z, w} = obj',
    run: n => {
      let sink = 0;
      for (let i = 0; i < n; i++) {
        let {x, y, z, w} = objPool[i & MASK];
        sink += x + y + z + w;
      }
      return sink;
    },
  },
  {
    name: 'let {x, ...rest} = obj',
    run: n => {
      let sink = 0;
      for (let i = 0; i < n; i++) {
        let {x, ...rest} = objPool[i & MASK];
        sink += x + rest.y;
      }
      return sink;
    },
  },
  {
    name: 'let {pos: {x, y}, meta: {id}} = obj',
    run: n => {
      let sink = 0;
      for (let i = 0; i < n; i++) {
        let {
          pos: {x, y},
          meta: {id},
        } = nestedPool[i & MASK];
        sink += x + y + id;
      }
      return sink;
    },
  },
  {
    name: 'let {x, q = 5} = obj (default)',
    run: n => {
      let sink = 0;
      for (let i = 0; i < n; i++) {
        let {x, q = 5} = objPool[i & MASK] as {x: number; q?: number};
        sink += x + q;
      }
      return sink;
    },
  },
  {
    name: 'let [a, b] = arr',
    run: n => {
      let sink = 0;
      for (let i = 0; i < n; i++) {
        let [a, b] = arrPool[i & MASK];
        sink += a + b;
      }
      return sink;
    },
  },
  {
    name: 'let [a, b, ...rest] = arr',
    run: n => {
      let sink = 0;
      for (let i = 0; i < n; i++) {
        let [a, b, ...rest] = arrPool[i & MASK];
        sink += a + b + rest.length;
      }
      return sink;
    },
  },
  {
    name: 'let [a, , c] = arr (hole)',
    run: n => {
      let sink = 0;
      for (let i = 0; i < n; i++) {
        let [a, , c] = arrPool[i & MASK];
        sink += a + c;
      }
      return sink;
    },
  },
  {
    name: 'let [a, b = 7] = arr (default)',
    run: n => {
      let sink = 0;
      for (let i = 0; i < n; i++) {
        let [a, b = 7] = shortArrPool[i & MASK];
        sink += a + b;
      }
      return sink;
    },
  },
  {
    name: '[a, b] = [b, a] (swap)',
    run: n => {
      let a = 1;
      let b = 2;
      for (let i = 0; i < n; i++) {
        [a, b] = [b, a + i];
      }
      return a + b;
    },
  },
  {
    name: 'fn({x, y}) param',
    run: n => {
      let sink = 0;
      for (let i = 0; i < n; i++) {
        sink += sumParams(objPool[i & MASK]);
      }
      return sink;
    },
  },
  {
    name: 'fn([a, b]) param',
    run: n => {
      let sink = 0;
      for (let i = 0; i < n; i++) {
        sink += sumArrayParams(arrPool[i & MASK]);
      }
      return sink;
    },
  },
  {
    name: 'for (const {x, y} of objs)',
    run: n => {
      let sink = 0;
      const rounds = Math.max(1, Math.floor(n / POOL_SIZE));
      for (let r = 0; r < rounds; r++) {
        for (const {x, y} of objPool) {
          sink += x + y;
        }
      }
      return sink;
    },
  },
  {
    name: 'for (const [a, b] of arrs)',
    run: n => {
      let sink = 0;
      const rounds = Math.max(1, Math.floor(n / POOL_SIZE));
      for (let r = 0; r < rounds; r++) {
        for (const [a, b] of arrPool) {
          sink += a + b;
        }
      }
      return sink;
    },
  },
  {
    name: 'baseline: obj.x + obj.y (no destructuring)',
    run: n => {
      let sink = 0;
      for (let i = 0; i < n; i++) {
        const o = objPool[i & MASK];
        sink += o.x + o.y;
      }
      return sink;
    },
  },
  {
    name: 'baseline: arr[0] + arr[1] (no destructuring)',
    run: n => {
      let sink = 0;
      for (let i = 0; i < n; i++) {
        const a = arrPool[i & MASK];
        sink += a[0] + a[1];
      }
      return sink;
    },
  },
];

export const BENCH_NAMES = benches.map(b => b.name);

let globalSink = 0;

export function runBench(index: number, iterations: number): BenchResult {
  const bench = benches[index];
  // Warm up so the measurement isn't dominated by first-run costs.
  globalSink += bench.run(Math.min(10_000, iterations));
  const start = now();
  globalSink += bench.run(iterations);
  const ms = now() - start;
  return {
    name: bench.name,
    iterations,
    ms,
    opsPerMs: ms > 0 ? iterations / ms : Infinity,
  };
}

export function getSink() {
  return globalSink;
}

Updated the logic for preserveDestructuring option to default to true if not specified.
@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Oct 9, 2026
@facebook-github-tools facebook-github-tools Bot added p: Callstack Partner: Callstack Partner Shared with Meta Applied via automation to indicate that an Issue or Pull Request has been shared with the team. labels Oct 9, 2026
@robhogan

robhogan commented Oct 9, 2026

Copy link
Copy Markdown
Collaborator

@vzaidman / @javache what's the status of this internally these days?

unstable_preserveDestructuring is toggled by a Buck modifier IIRC and we were going to run FB app experiments (for this and all the other flags) like I did for preserveClasses.

I'd be happy to see this transform turned off but I half recall some specific performance concerns (@tmikov?)

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. p: Callstack Partner: Callstack Partner Shared with Meta Applied via automation to indicate that an Issue or Pull Request has been shared with the team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants