supabase-js icon indicating copy to clipboard operation
supabase-js copied to clipboard

Tree shaking

Open lawrencecchen opened this issue 3 years ago • 25 comments

Feature request

Tree-shaking is currently not possible with the supabase-js library because SupabaseClient is implemented using classes. Neither Webpack or Rollup supports tree-shaking class methods.

Is your feature request related to a problem? Please describe.

Supabase bundle size is quite small compared to others (firebase, amplify), but it will only increase as more features are added. Projects that do not use all the features that Supabase provides will regardless pay the cost. Firebase has realized this with their new Modular Javascript SDK.

Describe the solution you'd like

Instead of initializing a single Supabase client, here's a modular API proposal:

// initSupabase.js
const supabaseClient = createClient(supabaseUrl, supabaseKey);
// Does not automatically instantiate RealtimeClient, PostgrestClient, etc.
// Features can be opted in as such:
export const db = getPostgrest(supabaseClient);
export const realtime = getRealtime(supabaseClient);
export const auth = getAuth(supabaseClient);
export const storage = getStorage(supabaseClient);
// some-random-component.js
import { db, realtime } from './initSupabase';

const { data, error } = await db
  .from('cities')
  .select()

const mySubscription = realtime
  .from('*')
  .on('*', payload => {
    console.log('Change received!', payload)
  })
  .subscribe()

While this won't eliminate every piece of unused code, it's a start to enabling opt-in for specific features.

Caveats

  • Will break the current stable API, but can be remedied w/ codemods.
  • Not sure if SupabaseAuthClient can be separated as all other modules rely on that

If this is something you guys think is worth pursuing, I'd be happy to start working on a PR!

lawrencecchen avatar Apr 10 '21 01:04 lawrencecchen

For the meantime, is there a supabase package available without realtime.js, as it increase bundle size significantly?

maxcodefaster avatar Aug 25 '21 16:08 maxcodefaster

@maxcodefaster you can use the client library for each components independently, but you need all for supabase-js.

What's common.js? There's no significant bloat in realtime-js as far as I'm aware.

soedirgo avatar Aug 25 '21 16:08 soedirgo

nvm. i got an error on angular cli stating that realtime-js relies on common.js and thus increases bundle size. i forked supabase-js and remove realtime-js references, if someone is interested:

https://www.npmjs.com/package/supabase-js-without-realtime-js

maxcodefaster avatar Aug 25 '21 18:08 maxcodefaster

... i forked supabase-js and remove realtime-js references, if someone is interested:

https://www.npmjs.com/package/supabase-js-without-realtime-js

@maxcodefaster , is your fork still up-to-date? supabase-js is currently the largest dependency of my app and I currently only use auth. Thanks!

micahjon avatar Mar 19 '22 23:03 micahjon

+1 on this. Also fixes the issue of "realtime" being imported in Next.js' Edge runtime.

sannajammeh avatar Jul 07 '22 01:07 sannajammeh

I am wondering, is the new supabase v2 RC solving tree shaking? I am seeing that the dist is actually a tiny bit larger than in v1.

v2 dist

Bildschirmfoto 2022-08-19 um 09 27 17

v1 dist

Bildschirmfoto 2022-08-19 um 09 27 26

I am seeing the ability to define certain settings for createClient

const options = {
  db: {
    schema: 'public',
  },
  auth: {
    autoRefreshToken: true,
    persistSession: true,
    detectSessionInUrl: true,
  },
  global: {
    headers: { 'x-my-custom-header': 'my-app-name' },
  },
}
const supabase = createClient(
  'https://xyzcompany.supabase.co',
  'public-anon-key',
  options
)

I am using only the database and storage function of supabase. Wouldn't it be great to allow something like:

const options = {
  db: true,
  auth: false,
  realtime: false,
}
const supabase = createClient(
  'https://xyzcompany.supabase.co',
  'public-anon-key',
  options
)

or like https://github.com/storyblok/storyblok-js#initialization does it

import { databaseFeature, storageFeature } from '@supabase/supabase-js/features'
const options = {
  useOnlyFeatures: [databaseFeature, storageFeature]
}
const supabase = createClient(
  'https://xyzcompany.supabase.co',
  'public-anon-key',
  options
)

madebyfabian avatar Aug 19 '22 07:08 madebyfabian

We weren't considering tree-shaking in v2, but might in v3. That said there seems to still be some low hanging fruits here - realtime-js seems to have doubled in size 🤔

soedirgo avatar Aug 23 '22 03:08 soedirgo

We weren't considering tree-shaking in v2

@soedirgo May I ask why? Supabase-js is a whopper. I'd much rather download the library on my server instead of making every single user download it in the browser.

binyamin avatar Oct 21 '22 21:10 binyamin

There are more pressing issues right now, esp. wrt functionality - once the client libs are stable enough we'll look into how to refactor them to support tree-shaking.

soedirgo avatar Oct 25 '22 05:10 soedirgo

+1, I'm using the supabase client solely for auth for a nextjs app. Would be nice to isolate the bits I'm not using :)

Screen Shot 2023-02-15 at 5 24 48 PM

Andrewjeska avatar Feb 15 '23 20:02 Andrewjeska

@soedirgo am I right to say that the bundle size is huge since of the npm websocket package(16k) which cause the entire size to be 26kb currently.

Since all browsers already support websocket, that should be easy win and fix and provide a browser build right? I would be happy to make a PR if you would consider it!

abhishiv avatar Mar 26 '23 17:03 abhishiv

Hmm, the websocket package isn't that big actually (1.1KB), but yeah that'd help trim it down. Though a bigger win would be from omitting cross-fetch (2.8KB) on environments that don't need it.

soedirgo avatar Mar 27 '23 05:03 soedirgo

Hmm, the websocket package isn't that big actually (1.1KB), but yeah that'd help trim it down. Though a bigger win would be from omitting cross-fetch (2.8KB) on environments that don't need it.

Agreed. Most frameworks already polyfill, its supported in all major browsers and even Node JS at this point. Most React libraries already target ES6 only and tell users to polyfill. The best solution would be adding some docs about polyfilling and omitting it altogether. There is no reason for a library to provide its own polyfill imho.

sannajammeh avatar Mar 28 '23 10:03 sannajammeh

Even better if you could install each module separately.

I'm trying to bundle a docker image, but realtime-js depends on bufferutils and utf-8-validate, both of which must be built from source during install. I don't need realtime-js, and I really don't want to install python, make, etc, just to tree-shake it all away later. I have to generate many permutations of my container per deploy, so the build-time really adds up (in dev time and CI/CD cost).

This doesn't necessarily mean everyone needs to install multiple packages. I'm sure a lot of people want just the one. But it wouldn't surprise me if others are hitting this docker issue.

mikestopcontinues avatar Jun 03 '23 16:06 mikestopcontinues

Hey @mikestopcontinues, if all you use is the REST API, maybe using https://github.com/hugomrdias/postgrest-url with fetch would work for your use case!

Just mentioning it since not sure what's the timeline for v3.

abhishiv avatar Jun 03 '23 18:06 abhishiv

Thanks @abhishiv, I ended up just creating a fork in the meantime.

BTW, @soedirgo, websocket isn't big, but it does cause the issue I described above. The package hard-requires bufferutil and utf-8-validate, while ws has better handling around those packages. It's also a better-maintained package overall, and created by the same guy who made bufferutil and utf-8-validate.

As it stands, websocket prevents supabase-js from easily being installed in slim/alpine arm64 docker images without jumping through hoops. It would make tree-shaking a perfectly viable solution, since it would circumvent needing to build binaries. Any chance of switching?

mikestopcontinues avatar Jun 04 '23 06:06 mikestopcontinues

Currently Supabase takes about 300ms to be required in a AWS lambda function

Being able to only require the part of Supabase could significantly decrease cold starts for many projects:

Screenshot 2023-08-18 at 17 05 02

require profile here

remorses avatar Aug 18 '23 15:08 remorses

Thanks all! Some updates on reducing the bundle size:

  • we're in the process of replacing cross-fetch with just node-fetch, which should shave around 3kB
  • we're planning to remove node-fetch altogether in v3
  • we're planning to replace websocket with ws, but we need to PoC this first - PRs welcome!

There's no timeline for tackling treeshaking at the moment, but we're open to proposals on how this could be done - ideally we don't have to sacrifice DX for this.

soedirgo avatar Aug 22 '23 09:08 soedirgo

For fetch you can use native-fetch to use the Nodejs 18 or browser native implementation when possible: https://github.com/achingbrain/native-fetch

remorses avatar Aug 22 '23 10:08 remorses

@soedirgo

ideally we don't have to sacrifice DX for this.

DX is nice, but apps are for users, not developers. Bundle size affects the user, so that should take a higher priority. DX is an extra. The deliverables are the important part.

binyamin avatar Aug 22 '23 23:08 binyamin

Here's a nice website for checking how large each part of supabase-js is when it's bundled in an application

Just click Build and then expand the Analysis collapsible: https://bundlejs.com/?q=%40supabase%2Fsupabase-js&treeshake=%5B%7BcreateClient%7D%5D&config=%7B%22analysis%22%3A%22sunburst%22%7D

These are the current stats:

name       gzip/size   %

gotrue     23kb/107kb  39%
realtime   14kb/ 55kb  20%
postgrest  10kb/ 41kb  15%
storage     7kb/ 33kb  12%
supabase    5kb/ 14kb  5%
functions   2kb/  6kb  2%

cross-fetch 4kb/ 15kb  5%

beeequeue avatar Aug 23 '23 19:08 beeequeue

@binyamin DX is nice, but apps are for users, not developers. Bundle size affects the user, so that should take a higher priority. DX is an extra. The deliverables are the important part.

I couldn't agree more. It's especially frustrating when you need to handle authentication. We have auth on the landing page, which means that the whole supabase SDK needs to load the first time the user visits, just to handle auth.

I really wished the supabase team would take this issue seriously and see it as a priority.

@soedirgo you were asking for proposals, but I think that the original proposal is completely fine. The functions could be shorter but in essence:

import { storage } from 'supabase';

storage(mySupabase).etc

another way:

mySupabase.with(storage).etc

it could also "enhance" the supabase instance:

let enhancedSupabase = mySupabase.with(storage, auth)

enhancedSupabase.etc

All proposals seem fine to me but other frameworks might already solve this in a better way.

enyo avatar Jan 30 '24 09:01 enyo

If you just use @supabase/postgrest-js, you get 12% over the wire size.

KTibow avatar Apr 25 '24 13:04 KTibow

Hey all, I had this crazy idea: seeing as we know better than the compiler exactly what functionality we're using from supabase-js, what if we could just hook into the build phase, strip out all references to the packages we don't want, and then hand it back to the compiler to finish its job tree-shaking out unused dependencies?

I tried doing exactly this with @rollup/plugin-delete, and lo and behold! Here's the results:

Before: image

After: image

For context, this is essential for me because my project is on Cloudflare workers/pages which has a very restrictive limit of 1mb max bundle size on a free plan (this means that any code included in the SSR bundle contributes to the 1mb limit).

Simply adding Supabase auth functionality to my relatively barebones project was enough to stop the CF Pages deployment (mainly because of the large, unnecessary dependencies @supabase/postgrest-js, @supabase/realtime-js, & @supabase/storage-js being included), until I implemented the hacky workaround described below:

Solution:

  1. If using a Vite/Rollup-based framework (typically this is most modern frameworks besides Next.js), install the @rollup/plugin-delete plugin, e.g. pnpm add -D @rollup/plugin-delete.
  2. Import it in your config file and depending on how your framework passes its options to Vite/Rollup, paste the following (obviously modify it depending on the dependencies you want to remove). E.g. here's how it's setup in my Astro project:
import del from "rollup-plugin-delete";

...

vite: {
        build: {
            rollupOptions: {
                plugins: [
                    replace({
                        values: {
                            // Remove references to Supabase Postgrest client
                            "import { PostgrestClient, } from '@supabase/postgrest-js';": "",
                            "this.rest = new PostgrestClient(\`${_supabaseUrl}/rest/v1\`, {\nheaders: this.headers,\nschema: settings.db.schema,\nfetch: this.fetch,\n});": "",
                            // Remove references to Supabase Realtime client
                            "import { RealtimeClient, } from '@supabase/realtime-js';": "",
                            "export * from '@supabase/realtime-js';": "",
                            "return new RealtimeClient(this.realtimeUrl, Object.assign(Object.assign({}, options), { params: Object.assign({ apikey: this.supabaseKey }, options === null || options === void 0 ? void 0 : options.params) }));": "",
                            // Remove references to Supabase Storage client
                            "import { StorageClient as SupabaseStorageClient } from '@supabase/storage-js';": "",
                            "return new SupabaseStorageClient(this.storageUrl, this.headers, this.fetch);": ""
                        },
                        preventAssignment: true,
                        delimiters: ["", ""],
                    })
                ]
            },
            minify: 'terser',
            terserOptions: {
                module: true
            }
        }

I've only just tried this but it seems to work well and has let me bypass the 1mb CF pages limit.

Disclaimer: Hasn't been rigorously tested (or tested at all, really), so use at your own risk.

Final thoughts: despite ostensibly appearing to be a very hacky workaround, perhaps it's actually a decent solution because it gives the developer full control over which unused dependencies to omit without having to maintain a fork or patched version.

It also doesn't seem like @supabase/supabase-js has a very frequent release cadence, so this code should remain stable until the proper fix is implemented (i.e. dropping use of classes so that the lib is statically analysable by rollup and other build tools)

edit: so it turns out that sb's auth functionality is dependent on a couple of methods from the supabase postgrest class & supabase realtime class, lol. So if you're implementing auth, then the supabase postgrest package is the only package you can "strip out". Oh well, better than nothing I suppose 🤣

edit 2: While going deeper down the supabase/JS isomorphic meta-framework rabbithole, I have come across (what I think) is another important consideration for implementing treeshaking

  • any implementation which replaces classes with functions must use a function-based factory pattern

The reason is because on the server, cross-request state pollution is a real risk. Atm it's concisely guarded against by the code in supabase-js which looks like:

export const createClient = (supabaseUrl, supabaseKey, options) => new SupabaseClient(supabaseUrl, supabaseKey, options)

This essentially ensures that a supabase client instance is never shared between requests.

While the same behaviour can be replicated using only objects and ES modules (which are always singleton), it does make it easier for whoever implements a non-class based version of this lib to footgun themselves (and the community) by not providing the same guarantees on the server against cross-request contamination

jkhaui avatar Jun 09 '24 23:06 jkhaui

I also came across same problem. we are only using Supabase for auth. its a waste bundling everything together.

Supabase need to provide a modular client side sdk like Firebase

kasvith avatar Jul 08 '24 09:07 kasvith