Skip to content

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

Closed
retyui wants to merge 2 commits into
react:mainfrom
retyui:patch-2
Closed

retyui wants to merge 2 commits 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:
Click to view 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;
}

@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?)

@vzaidman

Copy link
Copy Markdown
Contributor

@robhogan you are right. #56829 was made specifically to prepare this for an experiment. Let me see what I can do about it.

@retyui

retyui commented Oct 15, 2026

Copy link
Copy Markdown
Contributor Author

@vzaidman Do you have any progress with an internal experiment?

@vzaidman

Copy link
Copy Markdown
Contributor

I found that there was a concern that the bundle size increases with it, but the investigation for that did not continue. I'm not sure who can pick this up in this point. I will summarize my findings and share it with some teams to see if anyone wants to pursue this.

@robhogan

robhogan commented Oct 18, 2026 •

Copy link
Copy Markdown
Collaborator

I had AI do a bit of a deep dive on this, running benchmarks on a couple of real phones across a range of different usage patterns.

I'd say it's net good news, but does make bundles slightly larger in most cases (smaller JS, but more inlined iterator handling in HBC is bigger than shared babel helpers), and native is much slower at some (uncommon?) patterns, particularly array destructuring from array literals.

The wins on object destructuring and object rest look worth it though.

https://robhogan.github.io/reports/react-native-58965-destructuring/

robhogan added a commit to robhogan/reports that referenced this pull request Oct 18, 2026
Explains what Hermes' hidden fast-destructure flag changes, why no runtime gain is expected, its bundle-size effect on four apps (-0.01% to -0.05%) and its compatibility cost, and recommends against adopting it with react/react-native#58965.
@robhogan

robhogan commented Oct 18, 2026 •

Copy link
Copy Markdown
Collaborator

IMO, if we (React/Meta) are not planning to run full runtime experiments in prod on removing Babel transforms we theoretically don’t need, the default should be to (responsibly, safely) just ship the removal and monitor the result.

Even at the risk of slight short term regression, the performance ceiling is higher long term if we bring Hermes as close as we can to user code. As long as Babel is hiding the engine’s capabilities especially from Meta apps, they’re unlikely to get any attention.

@tmikov, @javache

(But re this PR @retyui - the crash when enableBabelRuntime: false, as identified in the report, blocks landing this as-is)

Updated the logic for preserveDestructuring option to default to true if not specified.
@retyui

retyui commented Oct 19, 2026

Copy link
Copy Markdown
Contributor Author

(But re this PR @retyui - the crash when enableBabelRuntime: false, as identified in the report, blocks landing this as-is)

@robhogan Thank you, the crash should be fixed now.

@meta-codesync

meta-codesync Bot commented Oct 19, 2026

Copy link
Copy Markdown

@javache has imported this pull request. If you are a Meta employee, you can view this in D126038210.

@meta-codesync meta-codesync Bot closed this in 73711b3 Oct 19, 2026
@react-native-bot

Copy link
Copy Markdown
Collaborator

This pull request was successfully merged by @retyui in 73711b3

When will my fix make it into a release? | How to file a pick request?

@react-native-bot react-native-bot added the Merged This PR has been merged. label Oct 19, 2026
@meta-codesync

meta-codesync Bot commented Oct 19, 2026

Copy link
Copy Markdown

@javache merged this pull request in 73711b3.

@retyui

retyui commented Oct 19, 2026

Copy link
Copy Markdown
Contributor Author

Thank you @javache

@javache

javache commented Oct 20, 2026

Copy link
Copy Markdown
Contributor

FYI, this caused a slight bundle size increase for us - if there's any ideas for how to claim this back (or ideas for Hermes bytecode efficiency), please share!

cc @tmikov

robhogan added a commit to robhogan/reports that referenced this pull request Oct 20, 2026
Attributes the size increase from react/react-native#58965 to Hermes' inline array-destructuring code (per-element done checks, defaults and register copies) using opcode diffs of RNTester and Mattermost, and lists ways Hermes could claim it back. The section is linkable at #bytecode-size.
@robhogan

Copy link
Copy Markdown
Collaborator

@javache a small bytecode size increase is consistent with what I saw benchmarking Mattermost and Bluesky.

It's all in the array destructuring and Hermes' more correct but verbose unwinding of iterators. I had AI add a bit to the report on the increase here (with some thoughts on what would help in Hermes, no idea how reliable that is but none look easy to roll out):

https://robhogan.github.io/reports/react-native-58965-destructuring/#bytecode-size

Another option is to reintroduce a more targeted version of the Babel transform that lowers the problem patterns but keeps native object destructuring.

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. Merged This PR has been merged. 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.

5 participants