@samitouri / QOS-React-1 / commits / 90863beaa8

Expand the architecture doc

ghstack-source-id: 6ab453a3c5e23677d0c178cf4ea464c7a9e7ebf6 Pull Request resolved: https://github.com/facebook/react-forget/pull/2932

Joe Savona committed May 3, 2024 at 10:46 UTC 90863beaa89789a7791337b33a4be1d567be5733
1 file changed +19 -1
compiler/docs/DESIGN_GOALS.md
+19 -1
@@ -7,7 +7,7 @@ This document describes the goals, design principles, and high-level architectur
7 The idea of React Compiler is to allow developers to use React's familiar declarative, component-based programming model, while ensuring that apps are fast by default. Concretely we seek to achieve the following goals:
8
9 * Bound the amount of re-rendering that happens on updates to ensure that apps have predictably fast peformance by default.
10 -* Keep startup time neutral with pre-React Compiler performance. Notably, this means holding code size increases and memoization low enough to not impact startup.
10 +* Keep startup time neutral with pre-React Compiler performance. Notably, this means holding code size increases and memoization overhead low enough to not impact startup.
11 * Retain React's familiar declarative, component-oriented programming model. Ie, the solution should not fundamentally change how developers think about writing React, and should generally _remove_ concepts (the need to use React.memo(), useMemo(), and useCallback()) rather than introduce new concepts.
12 * "Just work" on idiomatic React code that follows React's rules (pure render functions, the rules of hooks, etc).
13 * Support typical debugging and profiling tools and workflows.
@@ -34,3 +34,21 @@ Many aspects of the design follow naturally from the above goals:
34 * High-level code is more compact, and helps reduce the impact of compilation on application size
35 * High-level constructs that match what the developer wrote are easier to debug
36 * From the above, it then follows that the compiler's internal representation must also be high-level enough to be able to output the original high-level constructs. The internal representation is what we call a High-level Intermediate Representation (HIR) — a name borrowed from Rust Compiler. However, React Compiler's HIR is perhaps even more suited to this name, as it retains high-level information (distinguishing if vs logical vs ternary, or for vs while vs for..of) but also represents code as a control-flow graph with no nesting.
37 +
38 +## Architecture
39 +
40 +React Compiler has two primary public interfaces: a Babel plugin for transforming code, and an ESLint plugin for reporting violations of the Rules of React. Internally, both use the same core compiler logic.
41 +
42 +The core of the compiler is largely decoupled from Babel, using its own intermediate representations. The high-level flow is as follows:
43 +
44 +- Babel Plugin: Determines which functions in a file should be compiled, based on the plugion options and any local opt-in/opt-out directives. For each component or hook to be compiled, the plugin calls the compiler, passing in the original function and getting back a new AST node which will replace the original.
45 +- Lowering (BuildHIR): The first step of the compiler is to convert the Babel AST into React Compiler's primary intermediate representation, HIR (High-level Intermediate Representation). This phase is primarily based on the AST itself, but currently leans on Babel to resolve identifiers. The HIR preserves the precise order-of-evaluation semantics of JavaScript, resolves break/continue to their jump points, etc. The resulting HIR forms a control-flow graph of basic blocks, each of which contains zero or more consecutive instructions followed by a terminal. The basic blocks are stored in reverse postorder, such that forward iteration of the blocks allows predecessors to be visited before successors _unless_ there is a "back edge" (ie a loop).
46 +- SSA Conversion (EnterSSA): The HIR is converted to HIR form, such that all Identifiers in the HIR are updated to an SSA-based identifier.
47 +- Validation: We run various validation passes to check that the input is valid React, ie that it does not break the rules. This includes looking for conditional hook calls, unconditional setState calls, etc.
48 +- Optimization: Various passes such as dead code elimination and constant propagation can generally improve performance and reduce the amount of instructions to be optimized later.
49 +- Type Inference (InferTypes): We run a conservative type inference pass to identify certain key types of data that may appear in the program that are relevant for further analysis, such as which values are hooks, primitives, etc.
50 +- Inferring Mutable Ranges: Several passes are involved in determing groups of values that are created/mutated together and the set of instructions involved in creating/mutating those values. We call these groups "reactive scopes", and each can have one or more declarations (or occassionaly a reassignment).
51 +- Constructing/Optimizing Reactive Scopes: Once the compiler determines the set of reactive scopes, it then transforms the program to make these scopes explicit in the HIR. The code is later converted to a ReactiveFunction, which is a hybrid of the HIR and an AST. Scopes are further pruned and transformed. For example, the compiler cannot make hook calls conditional, so any reactive scopes that contain a hook call must be pruned. If two consecutive scopes will always invalidate together, we attempt to merge them to reduce overhead, etc.
52 +- Codegen: Finally, the ReactiveFunction hybrid HIR/AST is converted back to a raw Babel AST node, and returned to the Babel plugin.
53 +
54 +The ESLint plugin works similarly. For now, it effectively invokes the Babel plugin on the code and reports back a subset of the errors. The compiler can report a variety of errors, including that the code is simply invalid JavaScript, but the ESLint plugin filters to only show the React-specific errors.