Closed owenpearson closed 5 months ago
➤ Automation for Jira commented:
The link to the corresponding Jira issue is https://ably.atlassian.net/browse/SDK-3552
I don't fundamentally object to tree-shakability. But honestly the expected savings here seem not that exciting given the additional complexity to the user (and work to us)? 225kB down to 150kB. (Or 100-120 but that's including things like removing IE-compatibility code which would also benefit the non-treeshaked version, so not a fair comparison).
Might be worth flipping the order of things around? Like -- rather than doing tree-shaking first, then doing things like "Removing IE and ES3 compatibility code and some soft-deprecated APIs" to improve things further, why not start with the improvements that'll benefit everyone, then when that's done, reevaluate again to see how much then you'd get with tree-shaking ? ISTM there's quite a bit of heavy baggage in there we can cut quite easily -- eg if we can get rid of all that WordArray stuff now everyone's using browsers that support arraybuffer, that's a good 30kB right there (20kB of cryptojs imports and a bunch more in bufferutils & crypto simplification).
Talking of things to cut -- skimming through the file size history, in 1.2.4 we were at 179kB (minified); we jumped to 193kB with the es6 module conversion, then up to 220kB with the typescript conversion, which seems to have resulted in pages and pages of polyfills being added to the bundle, mostly for language features that we're not using (things like runtime support for transpiled async/awaits). ISTM worth reexamining what settings we transpile with to try and avoid the need for that?
I was wondering what this proposal would mean for our existing plugins mechanism, which also aims to "avoid excessively bloating the client library". Might we wish to consider removing the plugins mechanism and bringing the VCDIFF plugin into the ably-js codebase as an opt-in module?
I think that the proposal for splitting the library up into functional areas seems like a sensible one — the exact split I think depends on what our typical customer use cases look like and I guess @mikelee638 is best placed to answer that one.
I agree with @SimonWoolf that in terms of the order in which we do this work (i.e. tree shakability vs other stuff), it would make sense to start with stuff that is lower-hanging fruit and which impacts the largest number of users.
Thanks for this @owenpearson, there is some great thinking here.
Note: This post has been amended following @abhishiv pointing out my mistake of using Gzip sizes of other SDKs as opposed to raw minified size 🤦
My initial thoughts are:
.presence
wouldn't be available if the Presence class is not included. I was not aware that was possible. This has a bearing on current thinking we're having around how we should support additional capabilities like the Spaces SDKs with our SDKs. See comment thread at https://docs.google.com/document/d/1IpCJL7ME0JavWOjPL0VbsAVOy7nlO-A05H4Lf3vAbqA/edit?disco=AAAAu2535s0. Whilst we don't want to make this work bigger than it should be, rationalising how we deal with composable/shakable SDKs, plugins, and future SDKs for missions, will ensure we don't have all SDKs and teams going in different directions (arguably like we did with AAT).RealtimeSubscription
. Do we envisage a case where people would use Realtime but wouldn't subscribe or attach to channels? Have we had product input into what feature sets make sense? I think we should get that input so that the features explicitly imported are logical groupings of features.Auth
is not being included as a candidate for being split? Most web clients don't need any logic around how to issue Auth tokens etc. and simply get a token / API key, and use that for requests. I imagine there are lots of opportunities to optimise the code so that we have Auth (use tokens and API keys provided) and AuthProvider (issuing tokens etc).message.ts
for example, a large proportion of this code is logic to work with Cipher (because we did not choose to support plugins like we have in Ruby for exmaple), toString methods (convenience), vcDiff specific code, etc. IMHO we should be spending time making our code cleaner and more composable so that we don't leak stuff like this across our classes that makes it near on impossible to optimize.Happy to have a call to discuss this, especially if we need something more radical, to explore what's possible.
I've had a look through the above suggestions and analysis. My 2c:
ably.min.js
, which is 17% of the bundle size, whereas it's more like 7% for the pusher lib. Some of that is just data we can't drop (like URLs etc), but there's quite a lot of verbose error string literals, which the others seem to be much lighter on. Not sure if there's something we can do there? cloc
reports 12k LOC in ably-js, 6k LOC in Pusher, 8k LOC in PubNub. Still, that's a factor of 2x, and as Matt points out above, the difference in bundle size is much more.hey guys
I found this issue since I also don't like the fact that ably is larger compared to pusher/pubnub. But just wanted to say the factor is not 5-10x, since ably is 60kb gziped and pubnub is 45kb.
What I would ideally like is the ably protocol documented somewhere so i can connect to it directly via websocket - they way pusher has documented their protocol.
@abhishiv thanks for pointing out the mistake I made using Gzipped sizes when comparing against our minified byte size 🤦 I've updated my comment now to reflect your comments meaning we're in the ballpark of PubNub now in fact, without all the optimisation work we're doing which is encouraging!
In regards to the protocol documentation, we do in fact document it (it used to be part of our normal docs, but we moved it out as we found most devs weren't interested). See https://sdk.ably.com/builds/ably/specification/main/protocol/ and https://sdk.ably.com/builds/ably/specification/main/features/ for the complete spec.
This has been (mostly) implemented in ably-js v2 (too many PRs to link them all, so will mention them when necessary).
We now provide a modular variant of the library available at ably/modular
path. We decided to stick with existing plugins mechanism to provide modular functionality for the library.
Core plugins that are available as tree-shakable imports:
Regarding additional savings proposed by Owen in the original message:
XHRStreaming
and replacing upgrade mechanism with websocket + base fallback transport mechanismcrypto-js
dependency also has been removed in https://github.com/ably/ably-js/issues/1239 as we now use Web Crypto API.
Also, next idea has not been implemented:
It is possible, using TypeScript generics, to dynamically change the SDK API depending on which fields are defined on the object passed in as the second argument to BaseClient so that, for example, if a user creates a BaseClient with no RealtimePresence then they won't get .presence as an autocomplete option on a RealtimeChannel instance (and of course, TypeScript won't compile code which tries to access such properties).
Instead we check available plugins at runtime and throw an error if user tried to use an API that requires a plugin which was not provided. Would we want to export this idea to a separate issue @owenpearson ?
@owenpearson @lawrence-forooghian Once above questions above are resolved, would you be happy to close this issue?
Instead we check available plugins at runtime and throw an error if user tried to use an API that requires a plugin which was not provided. Would we want to export this idea to a separate issue (…)?
I remember discussing this with Owen during the design of v2 — I think he made an explicit decision to go with the error-throwing approach, so I don't think anything further is needed here.
Removing code related to realtime publishing
I had a go at this in https://github.com/ably/ably-js/pull/1504 and we decided not to proceed with it since the bundle size reduction was disappointing. So I think we can forget about it.
I think we can close this issue.
Closing as implemented in ably-js v2.
This RFC proposes the API and internal design for a tree-shakable ably-js client which will be included in version 2 of the library. Tree shaking is a form of dead code elimination used by module bundlers which works by statically analysis of ES6 import statements.
Tree shaking will allow users of the library to import features of ably-js independently, making it possible to import a 'minimal' ably-js client with only the necessary features. Since import size is more of a concern for the web browser environment, this proposal focusses on the ably-js web bundle, although the benefits will still be available to users on other platforms.
Constructor API
For backwards compatibility and convenience, the root import will still be an
Ably
object containingRest
andRealtime
constructors (with all features included):Additionally, all other fields currently present on the
Ably
object will be importable directly:A base client will be exported which contains the minimal code for a functional ably-js client. Other modules will be exported separately and may be passed as in an object as a second argument to the
BaseClient
constructor:It is possible, using TypeScript generics, to dynamically change the SDK API depending on which fields are defined on the object passed in as the second argument to
BaseClient
so that, for example, if a user creates aBaseClient
with noRealtimePresence
then they won't get.presence
as an autocomplete option on aRealtimeChannel
instance (and of course, TypeScript won't compile code which tries to access such properties).Modules available for tree-shaking
The ably-js automated bundle size report gives a helpful overview of how the web bundle is currently split in to modules. There are some cases where encapsulation of these modules is incomplete so the numbers are not precise, for example:
realtimechannel.ts
contains some presence related code, so just subtracting the size ofrealtime.presence.ts
andpresencemessage.ts
won't give you a precise figure for a client without any realtime presence functionality.This RFC proposes the following core modules will be available as tree-shakable imports:
BaseClient
contains auth, http client, utility classes, etcRealtimeChannelSubscription
contains classes necessary for realtime channel subscriptionRealtimePublishing
is available separately for publishing realtime messagesRealtimePresence
contains classes necessary for any realtime presence functionalityWebSocketTransport
is included by default in realtime modules while alternative transports (such asXHRStreamingTransport
) are optionally exported separatelyPush
,Presence
, etc) and all share some modules likePaginatedResource
Crypto
is available as a separate importMsgPackEncoder
is available as a separate importAdditionally, it is worth considering removing
XHRRequest
from the base client and making it optional withfetch
as the default.How much code would be eliminated?
A 'minimal' client is defined here as a realtime client which is able to subscribe to messages on a realtime channel. Currently, importing a v1 ably-js costs ~225kb. By inspecting modules from the bundle size report generated by 1ef4028aa7364089a74e80d9c1342378387c7f72 and calculating the total size of modules which would certainly not be included we would eliminate ~75kb.
Additional savings would come from:
realtimechannel.ts
It's difficult to estimate how significant the savings will be from the above list, but a rough estimate is that the resulting minimal client will be around 100-120kb.
┆Issue is synchronized with this Jira Task by Unito