[go: nahoru, domu]

Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
ludwigbacklund opened this issue May 27, 2021 · 75 comments
Open

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

ludwigbacklund opened this issue May 27, 2021 · 75 comments

Comments

@ludwigbacklund
Copy link

[REQUIRED] Describe your environment

  • Operating System version: macOS v11.3
  • Browser version: Chrome 90.0.4430.212
  • Firebase SDK version: firebase@9.0.0-beta.2
  • Firebase Product: auth

[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

@looptheloop88
Copy link

Hi @ludwigbacklund, thanks for the report. This iframe from Firebase Auth is used for third party login and eagerly loaded on mobile in certain situations. Currently, there’s no way to disable it. You may refer to this for your reference.

@ludwigbacklund
Copy link
Author

@looptheloop88 I see, thank you. However, I think you're a bit quick to close this. 263 kb of entirely unused code is a lot to download, parse and evaluate for nothing when our application has no use for it. I can imagine there are many other applications where this is true as well. For now we've had to use patch-package to manually disable the loading of this file which improved our Lighthouse score.

@looptheloop88
Copy link

Hi @ludwigbacklund, thanks for the feedback. We are exploring for potential solutions, however we can't provide definite timelines or details. I have reopened this issue and added a feature request label for the meantime.

@mark922
Copy link
mark922 commented Jun 19, 2021

@ludwigbacklund Can you share the snippet of your solution?

@ludwigbacklund
Copy link
Author

@mark922 Open node_modules/@firebase/auth/dist/esm2017/index-somehash.js. Find the _shouldInitProactively() function. Change return _isMobileBrowser() || _isSafari() || _isIOS(); to return false. Follow the patch-package README instructions to generate the patch and commit it.

@nikhilag
Copy link

Shouldn't this issue be given higher priority, since the benefit of firebase js package becoming smaller is nullified by this issue?

@sjbuysse
Copy link
sjbuysse commented Jul 6, 2021

@ludwigbacklund there are multiple index-somehash.js files, which one should I take? Or all of them? Why are there multiple? (they all look the same)

@ludwigbacklund
Copy link
Author

@ludwigbacklund there are multiple index-somehash.js files, which one should I take? Or all of them? Why are there multiple? (they all look the same)

Search in the @firebase/auth node_modules directory for _shouldInitProactively and you'll find it.

@nikhilag
Copy link

@looptheloop88 Could you please share if there's any update on this? If this issue is fixed, it will help us meet the core web vitals for an ecommerce site that we built for our client. As per pagespeed insights, if this is fixed, it will improve our LCP quite a bit (saw a very big improvement for desktop after moving to v9 but for mobile the score became worse).

@sam-gc
Copy link
Contributor
sam-gc commented Jul 12, 2021

Hi folks, the iframe code is in fact required if you're doing signInWithPassword or signInWithRedirect. If you're not using those methods (i.e. you're not using any of the OAuth providers), you can initialize Auth in a way that does not cause this code to load.

The default in getAuth() pulls in all dependencies you might need, including browserPopupRedirectResolver. You can tailor which dependencies are pulled in by using initializeAuth instead of getAuth.

The notes in the reference doc for initializeAuth explain in more detail, but at a high level you can mimic the behavior of getAuth() without pulling in the iframe code by using this code instead:

// initializeAuth throws an error if it's called more than once for the same app; save the reference.
const auth =initializeAuth(app, {
  persistence: [indexedDBLocalPersistence, browserLocalPersistence]
});

You can verify this by looking at the code:

return initializeAuth(app, {
popupRedirectResolver: browserPopupRedirectResolver,
persistence: [indexedDBLocalPersistence, browserLocalPersistence]
});

As you can see, the snippet I wrote earlier just omits the popupRedirectResolver dependency. This will prevent the iframe code from loading. But remember, this will prevent you from using signInWithPopup and signInWithRedirect.

@ArnaudD
Copy link
ArnaudD commented Jul 21, 2021

Hi @sam-gc, thank you for your response. Is it possible to delay the iframe code from loading until signInWithPopup or signInWithRedirect are called ?

@sam-gc
Copy link
Contributor
sam-gc commented Jul 21, 2021

In general it does delay. It only loads early in a few cases:

get _shouldInitProactively(): boolean {
// Mobile browsers and Safari need to optimistically initialize
return _isMobileBrowser() || _isSafari() || _isIOS();
}

@nikhilag
Copy link
nikhilag commented Jul 22, 2021

@sam-gc Is there no way to fix this such that lighthouse doesn't penalize performance score for using firebase sdk?

@ivanvanderbyl
Copy link

Any updates on this? We'd definitely like to disable it on mobile until it's really needed.

@sam-gc
Copy link
Contributor
sam-gc commented Sep 16, 2021

Hi folks, it is indeed possible to delay loading that code until the sign in methods are called. The popup and redirect family of methods take an optional third parameter that is the popupRedirectResolver. For example, the signature for signInWithPopup() is as follows:

export declare function signInWithPopup(auth: Auth, provider: AuthProvider, resolver?: PopupRedirectResolver): Promise<UserCredential>;

So instead of using getAuth() (or initializeAuth with the popupRedirectResolver set---these are the same), you can call initializeAuth() without the popupRedirectResolver option, then pass it in to the sign in functions as you need them. For example, this code below will always delay loading the iframe code until it is used:

import {initializeAuth, indexedDBLocalPersistence, browserLocalPersistence, browserSessionPersistence, browserPopupRedirectResolver, signInWithPopup, GoogleAuthProvider} from 'firebase/auth';
const auth = initializeAuth(app, {
 persistence: [
    indexedDBLocalPersistence,
    browserLocalPersistence,
    browserSessionPersistence
  ],
});

async function signIn() {
  const result = await signInWithPopup(auth, new GoogleAuthProvider(), browserPopupRedirectResolver);
}

@mdathersajjad
Copy link

If anyone is looking for a patch. https://gist.github.com/mdathersajjad/628c53913c10f7e090d52871faf1c373. And add browserPopupRedirectResolver as given in above comment

@abdo643-HULK
Copy link
abdo643-HULK commented Sep 27, 2021

Hi folks, it is indeed possible to delay loading that code until the sign in methods are called. The popup and redirect family of methods take an optional third parameter that is the popupRedirectResolver. For example, the signature for signInWithPopup() is as follows:

export declare function signInWithPopup(auth: Auth, provider: AuthProvider, resolver?: PopupRedirectResolver): Promise<UserCredential>;

So instead of using getAuth() (or initializeAuth with the popupRedirectResolver set---these are the same), you can call initializeAuth() without the popupRedirectResolver option, then pass it in to the sign in functions as you need them. For example, this code below will always delay loading the iframe code until it is used:

import {initializeAuth, indexedDBLocalPersistence, browserLocalPersistence, browserSessionPersistence, browserPopupRedirectResolver, signInWithPopup, GoogleAuthProvider} from 'firebase/auth';
const auth = initializeAuth(app, {
 persistence: [
    indexedDBLocalPersistence,
    browserLocalPersistence,
    browserSessionPersistence
  ],
});

async function signIn() {
  const result = await signInWithPopup(auth, new GoogleAuthProvider(), browserPopupRedirectResolver);
}

The only problem is that you'll get a pop-up blocked error on the first try to login with a provider on Safari. If you try again it works because by than the iframe has loaded.

@sam-gc
Copy link
Contributor
sam-gc commented Sep 27, 2021

@abdo643-HULK yep, that's one of the reasons why the _shouldInitProactively functionality exists

@abdo643-HULK
Copy link

@abdo643-HULK yep, that's one of the reasons why the _shouldInitProactively functionality exists

Where can I find this function ? I am looking through the source code of the auth but can't seem to find it. And thnks for your help

@sam-gc
Copy link
Contributor
sam-gc commented Sep 27, 2021

It's this code here:

get _shouldInitProactively(): boolean {
// Mobile browsers and Safari need to optimistically initialize
return _isMobileBrowser() || _isSafari() || _isIOS();
}

By omitting the popupRedirectResolver optional dependency in initializeAuth(), that function is never called

@Sdqumar
Copy link
Sdqumar commented Sep 28, 2021

Here goes my solution, i only dynamic import firebase auth, when I need it.
So I don't call it in the entire application, only when am making request to firebase.
If you have small to medium application, it should be to much work.
Tip* to keep the state of current user, use cookies
Screenshot_2021-09-28-06-02-55-097_com android chrome~2
Screenshot_2021-09-28-06-02-42-884_com android chrome~2

@vandres
Copy link
vandres commented Oct 30, 2021

Using the examples from the comments, I get an error "TypeError: class constructors must be invoked with 'new'"

        provideAuth(() => {
            const auth: Auth = initializeAuth(getApp(), {
                persistence: [
                    indexedDBLocalPersistence,
                    browserLocalPersistence,
                    browserSessionPersistence
                ],
                popupRedirectResolver: undefined
            });
            if (environment.emulator) {
                connectAuthEmulator(auth, 'http://localhost:9099', {disableWarnings: true});
            }

            return auth;
        }),

 const credential: UserCredential = await signInWithPopup(this.auth, new GoogleAuthProvider(), browserPopupRedirectResolver);

@vandres
Copy link
vandres commented Oct 30, 2021

Ah, the problem lies within AngularFire. Will open a ticket there (angular/angularfire#3038)

@Parakoos
Copy link
Parakoos commented Nov 3, 2021

Another reason why this can be problematic is if you try to put the whole Auth module into a web worker. It works at first, until you test it on mobile, when suddenly it breaks because the web worker has no access to window.

The use of initializeAuth instead of getAuth (as outlined by sam-gc) fixed the issue for me.

// initializeAuth throws an error if it's called more than once for the same app; save the reference.
const auth =initializeAuth(app, {
  persistence: [indexedDBLocalPersistence, browserLocalPersistence]
});

PS. Having auth in a web worker already means that you cannot use signInWithPopup or Redirect, so that is not a drawback. To make that work for me using Google, I basically use Google Auth scripts to get a token, then send that token to my web worker auth script where I use signInWithCredential instead. :-)

@kodai3
Copy link
kodai3 commented Jun 30, 2022

@SanjivG10

But that's not working well on Safari right ?

same as this
#4946 (comment)

@dosstx
Copy link
dosstx commented Jul 3, 2022

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

@danielo515
Copy link

@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
Copy link
Parakoos commented Jul 8, 2022

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
Copy link

@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
Copy link
danielo515 commented Jul 8, 2022

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
Copy link
rejhgadellaa commented Jul 8, 2022

You can find a small Comlink / firebase example in an issue I filed a while ago (since been resolved):
#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
Copy link

@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
Copy link

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

@sam-gc
Copy link
Contributor
sam-gc commented Aug 29, 2022

Hi folks, the comment in #4946 (comment) is the "correct" solution. See also this commonet: #4946 (comment).

Unfortunately, as you've noticed, this does not work in some cases like Safari. That is why this comment still applies: #4946 (comment). 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
Copy link
rejhgadellaa commented Aug 29, 2022

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
Copy link

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

brightowltutoring added a commit to brightowltutoring/sveltekit that referenced this issue Dec 14, 2022
…pendencies' and 'firebase/firebase-js-sdk#4946' I am now deferring the loading of 'signInWithPopup' iFrame ... which pagespeed insights complains about. This is done by swapping getAuth(app) for initializeAuth(app,..) and then contextually loading the 'browserPopupRedirectResolver' method and passing as a third parameter to 'signInWithPopup' in both google and twitter login functions
@raghavmri
Copy link

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

@prameshj
Copy link
Contributor

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 #4946 (comment), i see a next JS specific snippet in #4946 (comment)

@MarioAda
Copy link

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

@mhmo91
Copy link
mhmo91 commented May 3, 2023

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
Copy link
meropis commented Jun 23, 2023

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
Copy link
atelog commented Jul 15, 2023

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
Copy link

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

@DmitryUlyanov
Copy link

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
Copy link
ChrisChiasson commented Jan 30, 2024

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
Copy link
ChrisChiasson commented Jan 31, 2024

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
Copy link

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

@ChrisChiasson
Copy link

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
Copy link

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
Copy link

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests