Nazad na blog
3 min čitanja

Lenjo učitavanje (lazy-loading) reCAPTCHA-e uz Token Aliasing u Angularu

Integracija Google reCAPTCHA alata u aplikaciju obično je jednostavna.

Instalirate tuđu biblioteku, prosledite ključ, ubrizgate servis i gotovi ste.

Međutim, u pravim produkcionim sistemima, sama integracija je retko teži deo posla. Pravi izazov nastaje kada se performanse sretnu sa realnošću: pravim korisnicima, pravim uređajima, pravim mrežama.

Pravi problem: veličina skripte i vreme učitavanja

Google-ova reCAPTCHA skripta je relativno velika. Ako je učitavate na samom startu, kao što većina biblioteka i primera predlaže, to može negativno uticati na:

  • početno učitavanje stranice (initial page load)
  • vreme do interaktivnosti (time to interactive)
  • vizuelne performanse na sporijim uređajima

U našem slučaju, učitavanje skripte unapred izazvalo je vidljiva zamrzavanja UI-ja za neke korisnike u QA i produkcionom okruženju, iako je sve delovalo u redu pri lokalnom testiranju.

I to je ključni uvid: Dokumentacija je optimizovana za brzinu implementacije, ne za korisničko iskustvo.

Nije nam bila potrebna reCAPTCHA odmah pri učitavanju stranice. Bila nam je potrebna samo sekundu pre nego što korisnik pošalje formu. Cilj je postao jasan: Učitati reCAPTCHA skriptu samo i isključivo kada je stvarno potrebna.

Pravilno lenjo učitavanje (lazy-loading) reCAPTCHA-e

Umesto da se oslanjamo na biblioteke trećih strana, uveli smo mali interni sloj koji nam je pružio:

  • potpunu kontrolu nad tim kada se skripta učitava
  • centralizovani put izvršavanja
  • predvidiv API za ostatak aplikacije

U osnovi se nalaze apstraktni servis i konkretna Google implementacija.

Injection token za site key konfiguraciju:

export const RECAPTCHA_KEY = new InjectionToken('RECAPTCHA_KEY');

export function provideGrecaptcha(siteKey: string) {
  return [
    { provide: RECAPTCHA_KEY, useValue: siteKey },
    { provide: RecaptchaService, useClass: GoogleRecaptchaService }
  ];
}

Ovo čini konfiguraciju eksplicitnom i ostavlja je pogodnom za testiranje.

Apstraktni ugovor:

@Injectable()
export abstract class RecaptchaService {
  public abstract execute(action?: string): Promise;
  public abstract loadScript(): Promise;
}

Ostatak aplikacije se oslanja na nameru (intent), a ne na samu implementaciju.

Google reCAPTCHA implementacija:

declare const grecaptcha: {
  ready: (callback: () => void) => void;
  execute: (siteKey: string, options: { action: string }) => Promise;
};

@Injectable({ providedIn: 'root' })
export class GoogleRecaptchaService extends RecaptchaService {
  private document = inject(DOCUMENT);
  private siteKey = inject(RECAPTCHA_KEY);
  private renderer = inject(RendererFactory2).createRenderer(null, null);
  private isBrowser = isPlatformBrowser(inject(PLATFORM_ID));

  private scriptLoaded: Promise | null = null;

  execute(action: string = 'submit'): Promise {
    if (!this.isBrowser) {
      return Promise.resolve('');
    }

    return this.loadScript().then(() => {
      return new Promise((resolve, reject) => {
        grecaptcha.ready(() => {
          grecaptcha.execute(this.siteKey, { action }).then(resolve, reject);
        });
      });
    });
  }

  loadScript(): Promise {
    if (!this.scriptLoaded) {
      this.scriptLoaded = new Promise((resolve, reject) => {
        const script = this.renderer.createElement('script');
        script.src = `https://www.google.com/recaptcha/api.js?render=${this.siteKey}`;
        script.async = true;
        script.defer = true;
        script.onload = () => resolve();
        script.onerror = reject;
        this.renderer.appendChild(this.document.head, script);
      });
    }

    return this.scriptLoaded;
  }
}

U ovom trenutku, implementacija je stabilna, ali možemo dodatno unaprediti pristup zavisnostima. Umesto da eager load-ujemo konkretnu implementaciju svuda, možemo koristiti "token aliasing" i rezolovati servis isključivo kada zatreba.

Token aliasing u praksi

Angular nam omogućava da kažemo DI (Dependency Injection) sistemu: "Kad god neko zatraži RecaptchaService, daj mu stvarnu implementaciju."

Nakon toga, u komponenti:

@Component({
  selector: 'app-root',
  template: ``
})
export class App {
  private readonly injector = inject(Injector);

  async executeRecaptcha() {
    const recaptcha = this.injector.get(RecaptchaService);
    console.log(await recaptcha.execute());
  }
}

Zašto je ovo važno

  • Servis se rezoluje samo kada korisnik pokrene akciju
  • Nema bespotrebnog rada tokom inicijalizacije komponente
  • Čistiji mentalni model: bezbednosna logika se dešava u trenutku namere

Ovaj obrazac postaje posebno moćan kada su: funkcije uslovno uključene, skripte teške, ili su vam granice performansi izuzetno bitne.

Zaključak

Najveća lekcija ovde nije o reCAPTCHA alatu. Ovde je reč o načinu razmišljanja:

  • Ne učitavajte teški kod trećih strana osim ako ga stvarno ne koristite
  • Ne dozvolite da dokumentacija diktira vaše korisničko iskustvo
  • Koristite Angular DI sistem kao alat za optimizaciju performansi, a ne samo kao mehanizam za injektovanje zavisnosti

Token aliasing + lazy loading vam pružaju kontrolu, predvidivost i znatno bolje real-world performanse—tačno ono što želite u flow-ovima osetljivim na bezbednost.

P.S. Ovo rešenje savršeno funkcioniše i uz postojeće Angular biblioteke poput ng-recaptcha & ng-recaptcha2.

  • Identifikujte uska grla u performansama
  • Uočite probleme u arhitekturi
  • Dobijte konkretne preporuke
Pokreni Besplatnu Analizu