firebase / firebase-js-sdk

Firebase Javascript SDK
https://firebase.google.com/docs/web/setup
Other
4.82k stars 885 forks source link

Firebase v9 loads a large iframe.js file but only on mobile #4946

Open ludwigbacklund opened 3 years ago

ludwigbacklund commented 3 years ago

[REQUIRED] Describe your environment

[REQUIRED] Describe the problem

After upgrading from v8 to v9 a request for an iframe (https://[projectname].firebaseapp.com/__/auth/iframe.js) has started appearing on every page load, but only on mobile (via Chrome's simulated Device mode at least, and when Lighthouse is auditing the website).

An older Lighthouse report of our application, from before the upgrade to v9, did not mention this file at all so I can only assume it wasn't loaded back then.

This iframe file is big and seems unnecessary to our application since we never use any kind of iframing of Firebase functionality and the only auth login method we use is email login.

Is there a way to disable the loading of this iframe?

image

dosstx commented 2 years ago

Just wanted to say I am also dealing with this issue. Hoping for the best and will subscribe to this thread for updates!

danielo515 commented 2 years ago

@SanjivG10 as @kodai3 mentioned, that will have several popup blocked issues, forcing the user to first, unblock the popup, second, re-try the authentication, which will probably generate a lot of support tickets in anyone's app support team. I am testing the mentioned solution, and indeed the performance boost is HUGE, I went from 70 of performance to 97, and the time to first paint and to interactive were reduced significantly. Take a look at the screenshots:

image image

The only difference between those two screenshots is deferring the load of the popup. I wish I could just use this solution without any drawback, but seems that is not possible. I will also happily isolate the loading of all auth modules (even with the pre-loaded popup) to the login page, but that is not possible either because you will need the auth module to check the user auth status AND the auth module can not be initialized twice, so you have to either pull everything into your application or nothing.

Parakoos commented 2 years ago

The only difference between those two screenshots is deferring the load of the popup. I wish I could just use this solution without any drawback, but seems that is not possible. I will also happily isolate the loading of all auth modules (even with the pre-loaded popup) to the login page, but that is not possible either because you will need the auth module to check the user auth status AND the auth module can not be initialized twice, so you have to either pull everything into your application or nothing.

What I did was to isolate all firebase libraries in a Worker thread, that also loads the libraries lazily. Sure, now everything is asynchronous and like you said, the only way to check for user auth status is to load the library. So, everything that require user information is lazily loaded. I do, however, store some user information (very basic, like, are they logged in at all and what kind of access rights do I expect them to have) on local disk so that I can guess what menu options they will have. All that gets corrected once I get the real information, but this limits the redrawing since 99% of the time, the locally stored information is correct.

The worker thread also has the added benefit of keeping the main thread more open, and it forced me to create an API between the threads. Does mean it will be easy to swap out Firebase if I ever need to! :-)

rejhgadellaa commented 2 years ago

@Parakoos' solution is basically what I do. I only load auth + auth-ui in the main thread if the user is not logged in.

Slightly offtopic, but Comlink (by dasurma) will save you a lot of work when moving firebase into a worker

danielo515 commented 2 years ago

What I did was to isolate all firebase libraries in a Worker thread, that also loads the libraries lazily. Sure, now everything is asynchronous and like you said, the only way to check for user auth status is to load the library. So, everything that require user information is lazily loaded. I do, however, store some user information (very basic, like, are they logged in at all and what kind of access rights do I expect them to have) on local disk so that I can guess what menu options they will have. All that gets corrected once I get the real information, but this limits the redrawing since 99% of the time, the locally stored information is correct.

@Parakoos, running all the firebase related operations in a worker has been in my mind for some years now. Not only because the size of le libraries (which is huge, specially in the pre-v9 era) but because all of my projects are focused on being low cost but still need to compute statistics in slightly big datasets, so having all the async communication + big computations into a dedicated non blocking thread always looked very attractive to me. What was putting me away from it was that I was not sure about the investment (was it doable, or will I hit a roadblock after invested dozens of hours?) and because the complexity of the project which was already mature and big (not to mention the redux docs tell you to do all the calculations on the reducer but redux can not work in a worker... I should not have listen to them 😄 ). Now with your testimonial that is possible and starting a new and small project I feel more inclined to give t a go, and the existence of Comlink as pointed out by @rejhgadellaa really makes me want to try it out.

However, how do you manage the popup login scenario? I think I can maybe put all the auth logic into a lazy loaded component that, once the login is either verified or succeeded populates the app global store (or whatever you use) with the credentials and login information, and meanwhile just used whatever is cached in local storage? How are you sharing the credentials with the worker thread though? Do you send them from the main thread and re-initialize the firebase app there or how? I'm very interested in the topic

rejhgadellaa commented 2 years ago

You can find a small Comlink / firebase example in an issue I filed a while ago (since been resolved): https://github.com/firebase/firebase-js-sdk/issues/5791

Regarding auth in the main thread; I currently do it in a Preact app, and have one route (/login) where I send the user when the auth in the worker signals the user isn't logged in, then I load everything I need in that route:

import { initializeApp } from 'firebase/app';
import { getAuth } from 'firebase/auth';

// Init firebase app & auth
const app = initializeApp({/* config  */ );
const auth = getAuth(app);

// Import firebase ui dynamically so we can do SSR
const firebaseUi = await import('firebaseui');

// Init & start Firebase Auth UI
const ui = new firebaseUi.auth.AuthUI( auth );
ui.start('#firebaseui-auth-container', /* config */);

The above is just some parts of code I use, thrown together in a snippet, hope it helps :)

Update: Forgot the auth module :)

Parakoos commented 2 years ago

@danielo515 , I never load any firebase code in the main thread, so I don't use the AuthUI of course. Instead I use handmade login forms for email, google, facebook, twitter and github, get hold of the credentials, send them to the worker thread that uses them to log in to firebase. Basically, I use the 'Manual Flow' under all the Firebase Auth documentation. You can see this live at https://sharedgametimer.com/login

I did this because I didn't think I could do it any other way. I figured that logging the user in using AuthUI on the main thread wouldn't log the user in on the worker threat. But, seems @rejhgadellaa has gotten that to work (which would simplify the auth code A LOT). I do wonder if this means that the firebase core, app and auth modules are loaded twice? Once on the main thread, and again on the worker thread? I guess the download of the modules get cached (I'd hope) but parsing the JS has to happen twice, no? Is this a significant hit or a minor one?

Perhaps this conversation is getting a bit off-topic. I'd love to continue the discussion off-thread, @rejhgadellaa and @danielo515 . Email in my bio.

MartinXPN commented 2 years ago

Are there any updates on this issue? Would be really great to have faster performance for firebase on mobile.

sam-gc commented 2 years ago

Hi folks, the comment in https://github.com/firebase/firebase-js-sdk/issues/4946#issuecomment-1163262324 is the "correct" solution. See also this commonet: https://github.com/firebase/firebase-js-sdk/issues/4946#issuecomment-921147692.

Unfortunately, as you've noticed, this does not work in some cases like Safari. That is why this comment still applies: https://github.com/firebase/firebase-js-sdk/issues/4946#issuecomment-884498397. It is not easy to fix this at all, because the iframe and popup code is necessary for the proper functioning of social sign in. We're still aware of this issue, but there are no immediate plans/designs in place to avoid proactive initialization in general.

rejhgadellaa commented 2 years ago

FWIW, the iframe.js does have compression enabled (for some time now), but Lighthouse doesn't seem to pick it up and report it as uncompressed. So that's a false-positive.

I do wonder why I'm seeing 3 requests to iframe.js now (1x ~200b, 2x ~90kb, so one of them seems to be a dupe) but it's possible I have a bug in my code somewhere 😅

image

sundar-rajavelu commented 1 year ago

Adding AngularFireAnalytics > UserTrackingService in angular app > providers list loads the iframe immediately.

raghavmri commented 1 year ago

Hey, Is there any update on how to remove the load of iframe in next js?

prameshj commented 1 year ago

Hey, Is there any update on how to remove the load of iframe in next js?

Hi @raghavmri , the iframe.js file is necessary if you use signInWithPopup or signInWithRedirect.

Proactive loading of this file can be disabled by following the code snippet in https://github.com/firebase/firebase-js-sdk/issues/4946#issuecomment-921147692, i see a next JS specific snippet in https://github.com/firebase/firebase-js-sdk/issues/4946#issuecomment-1163262324

MarioAda commented 1 year ago

It's been more than 1.5 years. Will this ever be fixed?

mhmo91 commented 1 year ago

This is exactly why Big tech companies shouldn't be able to legally buy startups... 2 years later, and such an massive performance hit still unresolved!

meropis commented 1 year ago

Some more information I'd like to put forward if it helps:

  1. In order to help with debugging, this error persists for any browser that uses the webkit renderer. You can install a browser such as epiphany on linux where you can debug and test far more easily than loading up an IOS emulator etc;
  2. If you are running a localhost development environment you'll find that when the error is thrown, if you have an existing auth object saved you can simply hot reload the environment (I use vite) and the auth will work correctly, even in an iframe;
  3. CSP errors are fixable in most cases by using window.postMessage to make calls to external windows e.g. iframes. This is apparently not a possibility for this error after testing on my end. It is possible that someone with more experience may be able to find a way to pass the auth redirect back using postMessage and avoiding the CSP error, but I was unable to implement this.

I hope some of this will help in some way.

atelog commented 1 year ago

It is really bad that there is no simple solution to this problem. Why not simply give developers the option to dynamically import the module on demand?

As a temporary workaround in Angular, I conditionally provide the browserPopupRedirectResolver in the AppModule if the URL is the login page.

provideAuth(() => {
  if (typeof document !== 'undefined') {
    const isAuth = document.location.href.includes('auth');

    return initializeAuth(getApp(), {
      persistence: [
        indexedDBLocalPersistence,
        browserLocalPersistence,
        browserSessionPersistence,
      ],
      popupRedirectResolver: isAuth ? browserPopupRedirectResolver : undefined,
    });
  }
  return getAuth(getApp());
});

I also use the href attribute when navigating to the login page to reinitialize the Auth module. Additionally, I use window.location.href instead of the Angular router module in the guards.

This workaround improved the Lighthouse speed score, but it is super ugly.

SamTech37 commented 1 year ago

Shouldn't this issue be fixed already? It's taking forever!

DmitryUlyanov commented 11 months ago

Hi everyone,

The solutions above only work if you don't have Google auth. Here is hack to make it work even with Google auth. It just overrides the function that decides whether to preload html or not.

Object.defineProperty((browserPopupRedirectResolver as any).prototype, '_shouldInitProactively', {
    get: function () {
      return false;
    },
});
ChrisChiasson commented 7 months ago

I think we may be able to code our own workaround to separately call the _initialize method of browserPopupRedirectResolver at an arbitrary time if we could get a handle to an AuthInternal instance, which we might be able to find by inspecting auth dynamically in the console.

Specifically we would do something like the following--I'll report back what I find tomorrow: const authInternal = someAuthInternalInstance authInternal._popupRedirectResolver._initialize(authInternal)

References:

ChrisChiasson commented 7 months ago

Summary of investigation & solution:

Use the Object.defineProperty method from Dimitry above to turn off the iframe, but then use my next trick later to turn the iframe on before you need to do the redirect. This avoids needing to reinitialize auth--the part Danielo mentioned would be a blocker / isn't allowed.

The key trick to delayed initialization:

// here auth is the result of the standard getAuth, but prior to this you must have used Dimitry's method
auth._popupRedirectResolver._initialize(auth);

Step by Step Details:

import { initializeApp } from "firebase/app";
import { browserPopupRedirectResolver, getAuth } from "firebase/auth";

function get() {
  // double check printout in the mobile simulator or tethered console
  console.trace("get _shouldInitProactively overridden");
  return false;
}

// the method of Dimitry, one post above mine
Object.defineProperty(browserPopupRedirectResolver.prototype,
  '_shouldInitProactively', { get });

const firebaseConfig = { /*...*/ };
export const app = initializeApp(firebaseConfig);

const auth = getAuth();
console.log('auth._popupRedirectResolver before',
  JSON.stringify(auth._popupRedirectResolver));

// If the next code line is never called, iframe.js aka auth/iframe will not be found in Elements dev tools.
// If it is called it will be found near the end.
///////////////////////////////////////////////////////////////////////////////////////////////////////////
auth._popupRedirectResolver._initialize(auth); // this is the key line that can initialize the iframe later
///////////////////////////////////////////////////////////////////////////////////////////////////////////

console.log('auth._popupRedirectResolver after', auth._popupRedirectResolver);

Here is the key part wrapped up in a nice function that you could call unconditionally from some login component render method, but note that you must have already invoked Dimitry's method on startup prior to ever initializing auth (see step by step section above).

let resolverIsInitialized = false;

async function initializePopupRedirectResolverOnRender() {
  if (resolverIsInitialized) return;

  try {
    await auth.authStateReady();
    if (!resolverIsInitialized) auth._popupRedirectResolver._initialize(auth);
    resolverIsInitialized = true;
  } catch (error) {
    console.error(error);
  }
}
SpectorHacked commented 6 months ago

Its funny that its been almost 3 years, and still no fix, I am moving away to mongoDB just because of this issue.

ChrisChiasson commented 6 months ago

For others coming from search, the post above does fix the problem. Granted, it wasn't fixed in Firebase Auth by Google directly, but the problem of unconditionally loading the iframe.js on mobile startup is reduced to practice now using the setup in that post. You now have the option of loading it only when the login with redirect or popup is about to happen. It doesn't produce the errors discussed in prior solutions, and does not require re-initializing Auth or Firebase itself.

wahibimoh commented 5 months ago

the following hack worked for me, I used replace-in-file npm package to patch the built bundle js using the following script:

import replace from 'replace-in-file'
const options = {
  files: 'dist/assets/index*.js',
  from: /get\s*_shouldInitProactively\(\)\s*\{[^\}]*/g,
  to: 'get _shouldInitProactively() { return false',
};

const results = await replace(options)
console.log('Replacement results:', results);

I made it run automatically postbuild, it works with minified an unminified builds. I hope it helps someone.

satellite-xyz commented 3 months ago

Experiencing the same issue here, in particular within my next.js app. The initial loading time is way too slow, and it seems mainly attributed to this.