Following up on the note at the end of #17 ("worth a follow-up if that lazy behavior is actually wanted"). It is, and there's measured evidence from cru-bot that it's worth more than it might look.
The behaviour
controllers/index.js imports the controller at boot, the controller imports firebase/{app,auth,firestore} at module top level, so the SDK loads on every page of a cable app whether or not anything called firestore_stream_from. As #17 says, preload: false only keeps them out of the modulepreload set.
Measured
From a HAR captured on cru-bot's stage environment (the one that found #9). GET /conversations/new — a page with no stream element; firestore_stream_from appears only in conversations/show — fetched the controller and then all three modules:
| module |
uncompressed |
firebase-app.js |
103,065 |
firebase-auth.js |
158,756 |
firebase-firestore.js |
454,720 |
| total |
716,541 (~700 KiB) |
Those are content.size from the HAR, so uncompressed — gstatic serves them compressed, so wire cost is a fraction of that. But parse/compile cost is paid on the uncompressed bytes, and firebase-firestore.js alone is ~455 KiB of that.
Why it matters more than a typical dashboard app
cru-bot is public-facing and evangelistic: / is ConversationsController#new, the first thing a seeker sees, often on a phone. It has no stream. It currently pays for the entire Firestore SDK to render a text box. The pages that actually need cable are conversation show pages, which are reached after that first paint.
So the current shape puts the cost precisely where it has the least value and the most impact.
Shape of a fix
A dynamic import() inside connect() — i.e. the controller module stays cheap and pulls the SDK only when a stream element actually appears. #17 correctly flags that this is not free:
connect() becomes async, so the scheduleTokenFetch / queueMicrotask ordering needs a look.
connectEmulators must still run exactly once, before any sign-in or snapshot, and it currently sits in establishSession — which would now be downstream of an await.
- The
registry.app / registry.db memoisation would need to tolerate concurrent first-connects (two stream elements on one page racing the same dynamic import).
None of that looks hard, but it's the connect path, which is the part with the subtle ordering. Happy to be the guinea pig on cru-bot when it lands — it's the app with the most stream-free pages, so it's where the win shows up.
Not urgent
Nothing is broken; this is cost, not correctness. Filing it so the note in #17 doesn't get lost now that #12 is closed. If it helps, the data-solid-gcp-cable-listening / solid-gcp-cable:failed machinery from 0.5.0 gives a clean way to assert in the emulator system test that a lazily-loaded client still goes live — so the change is testable with what's already there.
Following up on the note at the end of #17 ("worth a follow-up if that lazy behavior is actually wanted"). It is, and there's measured evidence from cru-bot that it's worth more than it might look.
The behaviour
controllers/index.jsimports the controller at boot, the controller importsfirebase/{app,auth,firestore}at module top level, so the SDK loads on every page of a cable app whether or not anything calledfirestore_stream_from. As #17 says,preload: falseonly keeps them out of the modulepreload set.Measured
From a HAR captured on cru-bot's stage environment (the one that found #9).
GET /conversations/new— a page with no stream element;firestore_stream_fromappears only inconversations/show— fetched the controller and then all three modules:firebase-app.jsfirebase-auth.jsfirebase-firestore.jsThose are
content.sizefrom the HAR, so uncompressed — gstatic serves them compressed, so wire cost is a fraction of that. But parse/compile cost is paid on the uncompressed bytes, andfirebase-firestore.jsalone is ~455 KiB of that.Why it matters more than a typical dashboard app
cru-bot is public-facing and evangelistic:
/isConversationsController#new, the first thing a seeker sees, often on a phone. It has no stream. It currently pays for the entire Firestore SDK to render a text box. The pages that actually need cable are conversationshowpages, which are reached after that first paint.So the current shape puts the cost precisely where it has the least value and the most impact.
Shape of a fix
A dynamic
import()insideconnect()— i.e. the controller module stays cheap and pulls the SDK only when a stream element actually appears. #17 correctly flags that this is not free:connect()becomes async, so thescheduleTokenFetch/queueMicrotaskordering needs a look.connectEmulatorsmust still run exactly once, before any sign-in or snapshot, and it currently sits inestablishSession— which would now be downstream of an await.registry.app/registry.dbmemoisation would need to tolerate concurrent first-connects (two stream elements on one page racing the same dynamic import).None of that looks hard, but it's the connect path, which is the part with the subtle ordering. Happy to be the guinea pig on cru-bot when it lands — it's the app with the most stream-free pages, so it's where the win shows up.
Not urgent
Nothing is broken; this is cost, not correctness. Filing it so the note in #17 doesn't get lost now that #12 is closed. If it helps, the
data-solid-gcp-cable-listening/solid-gcp-cable:failedmachinery from 0.5.0 gives a clean way to assert in the emulator system test that a lazily-loaded client still goes live — so the change is testable with what's already there.