Apollo-client: Issues with tree-shaking in v3 beta

Created on 14 Dec 2019  路  14Comments  路  Source: apollographql/apollo-client

Intended outcome:

I would expect these bundles to be roughly the same size:

import {Observable} from 'apollo-boost'
console.log(Observable)

and:

import {Observable} from '@apollo/client'
console.log(Observable)

Actual outcome:

The apollo-boost bundle is 7.6 KiB, whereas the @apollo/client bundle is 94.2 KiB.

How to reproduce the issue:

Compare this branch to this branch. To produce the bundle size measurements, check out either branch and run npm run build.

Versions

For the apollo-boost bundle:

  System:
    OS: macOS Mojave 10.14.6
  Binaries:
    Node: 12.13.1 - ~/.config/nvm/12.13.1/bin/node
    npm: 6.13.4 - ~/.config/nvm/12.13.1/bin/npm
  Browsers:
    Chrome: 79.0.3945.79
    Firefox: 69.0.2
    Safari: 13.0.4
  npmPackages:
    apollo-boost: ^0.4.7 => 0.4.7 

The @apollo/client one is the same except:

  npmPackages:
    @apollo/client: 3.0.0-beta.34 => 3.0.0-beta.34
鉁嶏笍 working-as-designed 馃挰 discussion 馃摝 bundle size

Most helpful comment

Thanks for the additional details @billyjanitsch. Apollo Client 3 is tree-shakable, and working as expected. I'm not sure how the 7.6kB you mentioned for apollo-boost was calculated, but if you take a look at the following, you'll see they're pretty close:

AC3 adds more functionality in for sure, so it's a bit bigger. We're always working on reducing bundle sizes, and have changes coming that will help with this, but for now everything is working as expected.

As far as tree-shaking goes, if you want to validate it's working, you can npm i --save-dev unminified-webpack-plugin and update your webpack.config.js as:

const UnminifiedWebpackPlugin = require('unminified-webpack-plugin');

module.exports = {
  devtool: 'source-map',
  mode: 'production',
  plugins: [
    new UnminifiedWebpackPlugin()
  ]
}

Then after npm run build, if you open up the dist/main.nomin.js file and search for useQuery, you won't find anything. If you then adjust your index.js file as:

import { Observable, useQuery } from '@apollo/client';
console.log(Observable, useQuery)

then rebuild, open the dist/main.nomin.js file, and search for useQuery, you will see our useQuery React hook is now in the bundle.

All 14 comments

@billyjanitsch any chance you could try things out with @apollo/client >= 3.0.0? AC3 should be tree-shakeable. We have examples apps to verify this here: https://github.com/apollographql/apollo-client/tree/master/examples/bundling

If you're still noticing issues though, definitely let us know. Thanks!

Thanks for following up, @hwillson! I'll try it out this weekend and let you know.

@hwillson Unfortunately it's not fixed; it actually got even worse. 馃檨

After upgrading @apollo/client from 3.0.0-beta.34 to 3.0.2, the bundle size increased from 94.2 KiB to 109 KiB.

Here's a gist that may help with debugging. I took the bundle that gets built by my example repo at this SHA and ran it through Prettier to make it easier to read. It should help identify which parts of the client are included in the bundle beyond Observable.

Thanks for the additional details @billyjanitsch. Apollo Client 3 is tree-shakable, and working as expected. I'm not sure how the 7.6kB you mentioned for apollo-boost was calculated, but if you take a look at the following, you'll see they're pretty close:

AC3 adds more functionality in for sure, so it's a bit bigger. We're always working on reducing bundle sizes, and have changes coming that will help with this, but for now everything is working as expected.

As far as tree-shaking goes, if you want to validate it's working, you can npm i --save-dev unminified-webpack-plugin and update your webpack.config.js as:

const UnminifiedWebpackPlugin = require('unminified-webpack-plugin');

module.exports = {
  devtool: 'source-map',
  mode: 'production',
  plugins: [
    new UnminifiedWebpackPlugin()
  ]
}

Then after npm run build, if you open up the dist/main.nomin.js file and search for useQuery, you won't find anything. If you then adjust your index.js file as:

import { Observable, useQuery } from '@apollo/client';
console.log(Observable, useQuery)

then rebuild, open the dist/main.nomin.js file, and search for useQuery, you will see our useQuery React hook is now in the bundle.

Thanks for the reply, @hwillson! Unfortunately, I think we're talking past each other a little.

I'm not sure how the 7.6kB you mentioned for apollo-boost was calculated

I linked to the Boost branch in the OP. It's exactly the same source code as the AC branch. The only difference is that one depends on Boost and the other depends on AC. Both sizes are computed by running npm run build.

I understand that the _entire_ size of apollo-boost is comparable to the entire size of @apollo/client, which Bundlephobia verifies. My point isn't that AC is bigger than Boost; it's that AC fails to tree-shake a case that Boost was able to.

You can actually see this in your Bundlephobia links if you scroll down to the "Exports Analysis" sections. This section shows the size of the bundle if you only import each individual export from the library, rather than importing the whole thing. The reported size of the Observable export is 2.1kB for Boost and 21.1kB for AC. These are gzipped sizes so they're in the same ballpark as the sizes I reported (which are minified but not gzipped).

AC doesn't fail to tree-shake _all_ cases; only some of them. That's why this isn't evident from the fixtures in the repo.

Another way to verify this is by adding unminified-webpack-plugin to the AC branch as you suggested. I also checked the resulting bundle into the repo so that I can link to it.

Sure enough, even though the code only imports Observable, the resulting bundle contains many other parts of AC, such as:

You're right that useQuery does get tree-shaken here, but the majority of AC doesn't.

Got it @billyjanitsch, thanks for clarifying. This is all definitely known. We balanced a few tradeoffs when restructuring AC3, and are currently favoring the majority of use cases when using @apollo/client out of the box. Most people want the cache, HttpLink, gql, etc. If you're using the default @apollo/client entry point, we're assuming you want ApolloClient. If you want ApolloClient, then you're going to need other supporting code that can't be removed, like the code you mentioned in https://github.com/apollographql/apollo-client/issues/5686#issuecomment-660383250.

That being said, we do offer smaller entry points when people are looking for less. For example:

  • @apollo/client/core
  • @apollo/client/utilities
  • @apollo/client/cache
  • etc.

Your example of just wanting Observable is an interesting one. Observable comes from @apollo/client/utilities, so if you really just wanted it by itself, you would be better off importing it from @apollo/client/utilities directly. The being said though, it looks like Observable isn't currently exported from @apollo/client/utilities. It should be, so I'll fix that.

We're constantly looking for ways to improve all of this, so if you have any concrete suggestions on areas we can improve (that don't hinder the out of the box getting started @apollo/client experience), we're definitely all ears and would love the help. Thanks!

Heads up: we're not quite ready to publish @apollo/[email protected] yet, but I've published an @apollo/[email protected] release that I believe will resolve the Observable export issue, if you want to try that in the meantime. 馃檹

Hi @benjamn! Thanks for following up. Unfortunately, @apollo/[email protected] actually provides significantly worse tree-shaking than @apollo/[email protected] in this case. Compare the Bundlephobia results for the Observable export in 3.0.2 (21.1kB gzip) vs. 3.1.0-pre.0 (31.9kB gzip).

In fact, 3.1.0-pre.0 seems to provide worse tree-shaking results across the board. For example, the ApolloClient export has increased from 20.3kB to 27.3kB, and ApolloLink has increased from 5.7kB to 8.2.kB.

(Of course, the total size of @apollo/client has not changed significantly; this is just looking at individual exports.)

In any case, I appreciate the effort you've put in to fix this!


Your example of just wanting Observable is an interesting one.

@hwillson Thanks for your comment. To follow up on this point, Observable was just an example I chose to demonstrate the point. The way I actually ran into this was somewhat more complicated/realistic:

I had been precompiling all GraphQL queries in my app using babel-plugin-graphql-tag as recommended by the Apollo docs. This removed all imports of gql prior to bundling, and AC2 was therefore able to tree-shake away the dependencies on graphql-tag, etc. I was really happy with this arrangement since it also reduced the app initialization time (no longer had to parse N template strings on load).

When I upgraded to the AC3 beta, I noticed a massive increase in bundle size, and found that it was because this dependency, along with various other parts of AC I wasn't using, were no longer being tree-shaken.

I figured this specific scenario would be harder to explain in an issue, and I thought it might be mistaken for the incidental changes that AC3 made related to wrapping graphql-tag. So, instead, I found a single export that happened to demonstrate the same general point. Sorry if that ultimately made it harder to follow.

@billyjanitsch That may be related to #6687. I'll comment here when I've resolved that issue.

I've been looking for some free time to dig into how AC's modules are structured to try to help out. From a brief look, though, I have two ideas:

  1. Webpack's sideEffects optimization is not recursive in the way that people tend to expect. If package a depends on package b, and a declares "sideEffects": false but b does not, Webpack will, in certain cases, bail out of pruning not just b's modules but some of a's modules as well, depending on how they import from b and what they do with those imports.

    So, even though AC declares that its modules do not contain side-effects, some of its dependencies don't make this declaration, so the module restructure in AC3 may have caused Webpack to bail out of tree-shaking optimizations in more cases.

  2. Webpack's module-pruning mechanism is significantly less aggressive when imports from CJS modules are involved. Even if they've imported using ESM syntax, Webpack still understands them to be CJS. This causes it to inject interop code and largely bail out of module concatenation which generally is required for the DCE portion of tree-shaking.

    Files like this one are a bit worrisome because they mix re-exports of actual ESM modules with pseudo-re-exports of CJS ones (e.g. zen-observable). The mere presence of the latter may force Webpack to include more of the graph and rely on DCE rather than module pruning which is much less effective.

If I'm right about either or both of these theories, this issue should be fixable without sacrificing the top-level exports of AC3 providing the out-of-the-box experience you're looking for. The fix would be a matter of shuffling around some of the modules, particularly the re-exports.

@billyjanitsch Bundlephobia's Exports Analysis is bugged and not reliable: https://github.com/pastelsky/bundlephobia/issues/364

@benjamn any updates on this?

Was this page helpful?
0 / 5 - 0 ratings