## Fix: mobile signup modal freeze (defer Google Sign-In)

**Problem:** On mobile Safari, tapping "Sign up to create" on the create pages froze for 5–10s before the signup modal appeared. Desktop was fine.

**Cause:** The modal mounts Google's GSI sign-in widget (`<GoogleLogin>`), which boots a cross-site iframe to `accounts.google.com`. Mobile Safari's tracking prevention (ITP) throttles that cross-site init for several seconds, and because the widget mounted synchronously when the modal opened, the modal's first paint was blocked behind it. The login/signup pages didn't show this because they mount the widget during a route change, hiding the cost.

**Fix:** Defer mounting the real GSI iframe until after first paint (`useEffect` + `requestAnimationFrame`). The custom "Continue with Google" button shows instantly; the actual Google button hydrates a frame later, off the critical path. The container has a fixed height, so there's no layout shift.

- Centralized in `GoogleLoginButton`, so the login/signup pages benefit too.
- Tradeoff: the Google button becomes clickable ~1 frame later (imperceptible).
- No behavior change on desktop.
This commit is contained in:
BFlat committed 2026-05-26 19:34:36 +07:00
1 parent 33116e0e5b
commit 33be9f1ca4
1 file changed
+29 -14
@@ -8,7 +8,7 @@ import {
} from "@storyteller/common";
import { faSpinnerThird } from "@fortawesome/pro-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useState } from "react";
import { useEffect, useState } from "react";
interface GoogleLoginButtonProps {
onSuccess: (isNewUser: boolean) => void;
@@ -25,6 +25,17 @@ export function GoogleLoginButton({
}: GoogleLoginButtonProps) {
const [isLoading, setIsLoading] = useState(false);
// Defer mounting Google's GSI widget until after first paint. Its cross-site
// init stalls for several seconds on mobile Safari (ITP throttles
// accounts.google.com), so mounting it inline blocks the surrounding UI (e.g.
// the signup modal) from appearing. The custom visual button below shows
// instantly; the real Google button hydrates a frame later.
const [mountGsi, setMountGsi] = useState(false);
useEffect(() => {
const raf = requestAnimationFrame(() => setMountGsi(true));
return () => cancelAnimationFrame(raf);
}, []);
const handleGoogleSuccess = async (
credentialResponse: CredentialResponse,
) => {
@@ -75,19 +86,23 @@ export function GoogleLoginButton({
)}
</div>
{/* Google's actual button - invisible overlay that receives clicks */}
<div
className="absolute inset-0 w-full h-full overflow-hidden cursor-pointer flex items-center justify-center"
style={{ opacity: 0.01, zIndex: 10, transform: "scaleY(1.2)" }}
>
<GoogleLogin
onSuccess={handleGoogleSuccess}
onError={() => onError("Google login failed")}
size="large"
width={400}
text="continue_with"
/>
</div>
{/* Google's actual button - invisible overlay that receives clicks.
Mounted after first paint (see mountGsi) so its slow cross-site init
never blocks the surrounding UI from showing. */}
{mountGsi && (
<div
className="absolute inset-0 w-full h-full overflow-hidden cursor-pointer flex items-center justify-center"
style={{ opacity: 0.01, zIndex: 10, transform: "scaleY(1.2)" }}
>
<GoogleLogin
onSuccess={handleGoogleSuccess}
onError={() => onError("Google login failed")}
size="large"
width={400}
text="continue_with"
/>
</div>
)}
</div>
);
}