Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Let Metro handle import/export instead of Babel #1772

Merged
merged 2 commits into from
Oct 30, 2023
Merged

Conversation

gaearon
Copy link
Collaborator

@gaearon gaearon commented Oct 30, 2023

We currently rely on ESM -> CJS being compiled by Babel. This comes with some downsides.
I've tried using Metro's experimental built-in import / export setup and it seems to work.

Here's what this gives us.

Namespace imports are now lazy (as they should be)

In #1756, we enabled inline requires. However, initialization logs show all of these modules being prematurely initialized even though they're not being used. This is because Babel compiles import * to this:

  var ConfirmModal = _interopRequireWildcard(_$$_REQUIRE(_dependencyMap[9], "./Confirm"));
  var EditProfileModal = _interopRequireWildcard(_$$_REQUIRE(_dependencyMap[10], "./EditProfile"));
  var ProfilePreviewModal = _interopRequireWildcard(_$$_REQUIRE(_dependencyMap[11], "./ProfilePreview"));
  var ServerInputModal = _interopRequireWildcard(_$$_REQUIRE(_dependencyMap[12], "./ServerInput"));
  // ...
  function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function _getRequireWildcardCache(nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); }
  function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; }

So the _interopRequireWildcard Babel helper was causing the inline requires to evaluate early.

After the change, we can see these imports being inlined at their usage site, as you would expect:

    if ((activeModal == null ? void 0 : activeModal.name) === 'confirm') {
      snapPoints = _$$_IMPORT_ALL(_dependencyMap[12], "./Confirm").snapPoints;
      element = /*#__PURE__*/_jsx(_$$_IMPORT_ALL(_dependencyMap[12], "./Confirm").Component, _objectSpread({}, activeModal));
    } else if ((activeModal == null ? void 0 : activeModal.name) === 'edit-profile') {
      snapPoints = _$$_IMPORT_ALL(_dependencyMap[13], "./EditProfile").snapPoints;
      element = /*#__PURE__*/_jsx(_$$_IMPORT_ALL(_dependencyMap[13], "./EditProfile").Component, _objectSpread({}, activeModal));
    } else if ((activeModal == null ? void 0 : activeModal.name) === 'profile-preview') {
      snapPoints = _$$_IMPORT_ALL(_dependencyMap[14], "./ProfilePreview").snapPoints;
      element = /*#__PURE__*/_jsx(_$$_IMPORT_ALL(_dependencyMap[14], "./ProfilePreview").Component, _objectSpread({}, activeModal));
      needsSafeTopInset = true; // Need to align with the target profile screen.
    } else if ((activeModal == null ? void 0 : activeModal.name) === 'server-input') {
      snapPoints = _$$_IMPORT_ALL(_dependencyMap[15], "./ServerInput").snapPoints;
      element = /*#__PURE__*/_jsx(_$$_IMPORT_ALL(_dependencyMap[15], "./ServerInput").Component, _objectSpread({}, activeModal));
    // ...

React/RN built-in imports are now eager (as they should be)

In #1756, we regressed by having common APIs like View/Text and React's JSX runtime itself (jsx calls) go through new indirections at runtime, for example:

  function _react() {
    var data = _$$_REQUIRE(_dependencyMap[0], "@babel/runtime/helpers/interopRequireDefault")(_$$_REQUIRE(_dependencyMap[2], "react"));
    _react = function _react() {
      return data;
    };
    return data;
  }
  function _reactNative() {
    var data = _$$_REQUIRE(_dependencyMap[3], "react-native");
    _reactNative = function _reactNative() {
      return data;
    };
    return data;
  }
  // ...
  function _api() {
    var data = _$$_REQUIRE(_dependencyMap[7], "@atproto/api");
    _api = function _api() {
      return data;
    };
    return data;
  }
  // ...
  function _jsxRuntime() {
    var data = _$$_REQUIRE(_dependencyMap[9], "react/jsx-runtime");
    _jsxRuntime = function _jsxRuntime() {
      return data;
    };
    return data;
  }


    // ...

    return /*#__PURE__*/(0, _jsxRuntime().jsxs)(_reactNative().Pressable, {
      testID: `feed-${item.displayName}`,
      accessibilityRole: "button",
      style: [styles.container, pal.border, style],
      onPress: function onPress() {
        navigation.push('CustomFeed', {
          name: item.data.creator.did,
          rkey: new (_api().AtUri)(item.data.uri).rkey
        });
      },

After this change, both the jsx helpers and RN's built-ins are resolved eagerly:

  var React = _$$_IMPORT_DEFAULT(_dependencyMap[0], "react");
  var _reactNative = _$$_REQUIRE(_dependencyMap[1], "react-native"),
    Pressable = _reactNative.Pressable,
    StyleSheet = _reactNative.StyleSheet,
    View = _reactNative.View;
  var _jsx = _$$_REQUIRE(_dependencyMap[2], "react/jsx-runtime").jsx;
  var _jsxs = _$$_REQUIRE(_dependencyMap[2], "react/jsx-runtime").jsxs;

    // ...

    return /*#__PURE__*/_jsxs(Pressable, { // <---- no need to resolve _jsxs or Pressable during render
      testID: `feed-${item.displayName}`,
      accessibilityRole: "button",
      style: [styles.container, pal.border, style],
      onPress: function onPress() {
        navigation.push('CustomFeed', {
          name: item.data.creator.did,
          rkey: new (_$$_REQUIRE(_dependencyMap[9], "@atproto/api").AtUri)(item.data.uri).rkey
        });
      },

As you can see, our own modules like @atproto/api are still initialized lazily.

Test plan

Walked around the app. Both iOS and Android on the simulator. Seemed fine.

This doesn't affect web.

Perf impact

This gets us down from 1449 to 1397 modules being eagerly initialized during startup. As a result, I'm observing a ~200ms reduction in PROD Android startup time. Nothing major but should scale up better as we add more code.

Follow-ups

I've noticed duplicated _objectSpread helpers everywhere in our bundle. I think we might need to externalize the Babel runtime. Also, isn't Hermes modern enough to support spread by default? I'll look at that later to keep this PR scoped.

@gaearon gaearon requested a review from pfrazee October 30, 2023 21:23
Copy link
Collaborator

@pfrazee pfrazee left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

@gaearon
Copy link
Collaborator Author

gaearon commented Oct 30, 2023

Looks like something's up with the test setup 👀. Will have a closer look.

@ecreeth
Copy link

ecreeth commented Oct 30, 2023

Isn't Hermes modern enough to support spread by default?

Yes, Hermes does support it. The problem here is that the react-native-babel-preset hasn't being updated with the recent Hermes features. For now you need to use the unstable_transformProfile

@gaearon
Copy link
Collaborator Author

gaearon commented Oct 30, 2023

Doesn't Expo already set it by default to hermes-stable when Hermes is the engine?

@gaearon
Copy link
Collaborator Author

gaearon commented Oct 30, 2023

Ahh I see we're on an old version.

@gaearon gaearon merged commit 84ee640 into main Oct 30, 2023
4 checks passed
@gaearon gaearon deleted the metro-import-export branch October 30, 2023 21:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants