next_custom_transforms/transforms/
optimize_server_react.rs

1// This transform optimizes React code for the server bundle, in particular:
2// - Removes `useEffect` and `useLayoutEffect` calls
3// - Refactors `useState` calls (under the `optimize_use_state` flag)
4
5use serde::Deserialize;
6use swc_core::{
7    common::DUMMY_SP,
8    ecma::{
9        ast::*,
10        visit::{fold_pass, Fold, FoldWith},
11    },
12};
13
14#[derive(Clone, Debug, Deserialize)]
15pub struct Config {
16    pub optimize_use_state: bool,
17}
18
19pub fn optimize_server_react(config: Config) -> impl Pass {
20    fold_pass(OptimizeServerReact {
21        optimize_use_state: config.optimize_use_state,
22        ..Default::default()
23    })
24}
25
26#[derive(Debug, Default)]
27struct OptimizeServerReact {
28    optimize_use_state: bool,
29    react_ident: Option<Id>,
30    use_state_ident: Option<Id>,
31    use_effect_ident: Option<Id>,
32    use_layout_effect_ident: Option<Id>,
33}
34
35fn effect_has_side_effect_deps(call: &CallExpr) -> bool {
36    if call.args.len() != 2 {
37        return false;
38    }
39
40    // We can't optimize if the effect has a function call as a dependency:
41    // useEffect(() => {}, x())
42    if let box Expr::Call(_) = &call.args[1].expr {
43        return true;
44    }
45
46    // As well as:
47    // useEffect(() => {}, [x()])
48    if let box Expr::Array(arr) = &call.args[1].expr {
49        for elem in arr.elems.iter().flatten() {
50            if let ExprOrSpread {
51                expr: box Expr::Call(_),
52                ..
53            } = elem
54            {
55                return true;
56            }
57        }
58    }
59
60    false
61}
62
63fn wrap_expr_with_env_prod_condition(call: CallExpr) -> Expr {
64    // Wrap the call expression with the condition
65    // turn it into `process.env.__NEXT_PRIVATE_MINIMIZE_MACRO_FALSE && <call>`.
66    // And `process.env.__NEXT_PRIVATE_MINIMIZE_MACRO_FALSE` will be treated as `false` in
67    // minification. In this way the expression and dependencies are still available in
68    // compilation during bundling, but will be removed in the final DEC.
69    Expr::Bin(BinExpr {
70        span: DUMMY_SP,
71        left: Box::new(Expr::Member(MemberExpr {
72            obj: (Box::new(Expr::Member(MemberExpr {
73                obj: (Box::new(Expr::Ident(Ident {
74                    sym: "process".into(),
75                    span: DUMMY_SP,
76                    ..Default::default()
77                }))),
78                prop: MemberProp::Ident(IdentName {
79                    sym: "env".into(),
80                    span: DUMMY_SP,
81                }),
82                span: DUMMY_SP,
83            }))),
84            prop: (MemberProp::Ident(IdentName {
85                sym: "__NEXT_PRIVATE_MINIMIZE_MACRO_FALSE".into(),
86                span: DUMMY_SP,
87            })),
88            span: DUMMY_SP,
89        })),
90        op: op!("&&"),
91        right: Box::new(Expr::Call(call)),
92    })
93}
94
95impl Fold for OptimizeServerReact {
96    fn fold_module_items(&mut self, items: Vec<ModuleItem>) -> Vec<ModuleItem> {
97        let mut new_items = vec![];
98
99        for item in items {
100            new_items.push(item.clone().fold_with(self));
101
102            if let ModuleItem::ModuleDecl(ModuleDecl::Import(import_decl)) = &item {
103                if import_decl.src.value != "react" {
104                    continue;
105                }
106                for specifier in &import_decl.specifiers {
107                    if let ImportSpecifier::Named(named_import) = specifier {
108                        let name = match &named_import.imported {
109                            Some(n) => match &n {
110                                ModuleExportName::Ident(n) => n.sym.to_string(),
111                                ModuleExportName::Str(n) => n.value.to_string(),
112                            },
113                            None => named_import.local.sym.to_string(),
114                        };
115
116                        if name == "useState" {
117                            self.use_state_ident = Some(named_import.local.to_id());
118                        } else if name == "useEffect" {
119                            self.use_effect_ident = Some(named_import.local.to_id());
120                        } else if name == "useLayoutEffect" {
121                            self.use_layout_effect_ident = Some(named_import.local.to_id());
122                        }
123                    } else if let ImportSpecifier::Default(default_import) = specifier {
124                        self.react_ident = Some(default_import.local.to_id());
125                    }
126                }
127            }
128        }
129
130        new_items
131    }
132
133    fn fold_expr(&mut self, expr: Expr) -> Expr {
134        if let Expr::Call(call) = &expr {
135            if let Callee::Expr(box Expr::Ident(f)) = &call.callee {
136                // Mark `useEffect` as DCE'able
137                if let Some(use_effect_ident) = &self.use_effect_ident {
138                    if &f.to_id() == use_effect_ident && !effect_has_side_effect_deps(call) {
139                        // return Expr::Lit(Lit::Null(Null { span: DUMMY_SP }));
140                        return wrap_expr_with_env_prod_condition(call.clone());
141                    }
142                }
143                // Mark `useLayoutEffect` as DCE'able
144                if let Some(use_layout_effect_ident) = &self.use_layout_effect_ident {
145                    if &f.to_id() == use_layout_effect_ident && !effect_has_side_effect_deps(call) {
146                        return wrap_expr_with_env_prod_condition(call.clone());
147                    }
148                }
149            } else if let Some(react_ident) = &self.react_ident {
150                if let Callee::Expr(box Expr::Member(member)) = &call.callee {
151                    if let box Expr::Ident(f) = &member.obj {
152                        if &f.to_id() == react_ident {
153                            if let MemberProp::Ident(i) = &member.prop {
154                                // Mark `React.useEffect` and `React.useLayoutEffect` as DCE'able
155                                // calls in production
156                                if i.sym == "useEffect" || i.sym == "useLayoutEffect" {
157                                    return wrap_expr_with_env_prod_condition(call.clone());
158                                }
159                            }
160                        }
161                    }
162                }
163            }
164        }
165
166        expr.fold_children_with(self)
167    }
168
169    // const [state, setState] = useState(x);
170    // const [state, setState] = React.useState(x);
171    fn fold_var_declarator(&mut self, decl: VarDeclarator) -> VarDeclarator {
172        if !self.optimize_use_state {
173            return decl;
174        }
175
176        if let Pat::Array(array_pat) = &decl.name {
177            if array_pat.elems.len() == 2 {
178                if let Some(box Expr::Call(call)) = &decl.init {
179                    if let Callee::Expr(box Expr::Ident(f)) = &call.callee {
180                        if let Some(use_state_ident) = &self.use_state_ident {
181                            if &f.to_id() == use_state_ident && call.args.len() == 1 {
182                                // We do the optimization only if the arg is a literal or a
183                                // type that we can
184                                // be sure is not a function (e.g. {} or [] lit).
185                                // This is because useState allows a function as the
186                                // initialiser.
187                                match &call.args[0].expr {
188                                    box Expr::Lit(_) | box Expr::Object(_) | box Expr::Array(_) => {
189                                        // const [state, setState] = [x, () => {}];
190                                        return VarDeclarator {
191                                            definite: false,
192                                            name: decl.name.clone(),
193                                            init: Some(Box::new(Expr::Array(ArrayLit {
194                                                elems: vec![
195                                                    Some(call.args[0].expr.clone().into()),
196                                                    Some(
197                                                        Expr::Arrow(ArrowExpr {
198                                                            span: DUMMY_SP,
199                                                            body: Box::new(BlockStmtOrExpr::Expr(
200                                                                Box::new(Expr::Lit(Lit::Null(
201                                                                    Null { span: DUMMY_SP },
202                                                                ))),
203                                                            )),
204                                                            is_async: false,
205                                                            is_generator: false,
206                                                            params: vec![],
207                                                            ..Default::default()
208                                                        })
209                                                        .into(),
210                                                    ),
211                                                ],
212                                                span: DUMMY_SP,
213                                            }))),
214                                            span: DUMMY_SP,
215                                        };
216                                    }
217                                    _ => {}
218                                }
219                            }
220                        }
221                    }
222                }
223            }
224        }
225
226        decl.fold_children_with(self)
227    }
228}