, ,

Kako sam sestri napravio luksuzni webshop i upao u Firestore pakao (Postupak optimizacije performansi i kvota)

·

Svi znamo onaj osjećaj kad odlučiš napraviti projekt iz čistog inata. Moja sestra Goga izrađuje predivan unikatni nakit na Cresu (čiji cijeli katalog možete pogledati na njezinom predivnom novom webshopu). Njezin zaštitni znak su privjesci, naušnice i prstenje od kristalno prozirne epoksidne smole unutar koje su zauvijek zarobljeni stvarni komadići jadranskog raja – sitne školjke, morske zvjezdice, kamenčići, sušena kadulja i ostali otočni "morski inventar" koji sama pronalazi na plažama.

Kad se u obitelji povela priča o pokretanju webshopa, svi su nudili klasična, „glupa” rješenja: zakupi Shopify i plaćaj mjesečnu pretplatu i postotak od ionako teškog umjetničkog rada, ili postavi spori WooCommerce na neki jeftini dijeljeni hosting, natrpaj dvadeset dodataka i moli se da stranica ne učitava pet sekundi.

Kao inženjeru, to mi je bilo neprihvatljivo. Htio sam joj napraviti sustav koji leti – React (TypeScript + Vite) na frontendu, Firebase (Firestore i Auth) na backendu, ugrađena WASM Cro-Stem arhitektura za munjevitu lokalnu pretragu i hosting na brzim Cloudflare Pages. Nula kuna troškova infrastrukture za mali obrt, a performanse na razini svemirskog broda.

Početna stranica s elegantnim

No, ubrzo sam upao u klasičnu Firebase zečju rupu. U ovom postu proći ćemo kroz stvarni postupak kako sam spasio projekt od zloglasne Quota limit exceeded greške, drastično smanjio mrežni promet i migrirao bazu sa sporih Base64 slikovnih zapisa na Firebase Storage bez ručnog prepisivanja podataka.


1. Prva zečja rupa: Kad HMR i Strict Mode odluče spržiti tvoju kvotu

Prva verzija webshopa koristila je Firestoreov onSnapshot real-time listener za povlačenje proizvoda i novosti na klijentu. Logika je bila jednostavna: ako sestra ažurira cijenu ili doda proizvod u admin panelu, kupcima se to odmah prikazuje na ekranu bez ručnog osvježavanja stranice.

No, u lokalnom razvoju se to pretvorilo u tihog ubojicu kvota. Zbog kombinacije Viteovog Hot Module Replacementa (HMR) i Reactovog Strict Modea koji dvaput okida kuke (useEffect), svaki put kad bih spremio datoteku u kodu, React bi ponovno montirao komponente i otvarao nove listenere. Svaki reload u razvoju povlačio je cijelu bazu iznova, generirajući tisuće bespotrebnih čitanja baze podataka po minuti.

Kada na to dodaš činjenicu da je stara Firestore baza greškom bila dio "AI Grupe" (što je uzrokovalo pozadinsko indeksiranje i automatsko skeniranje od strane vanjskih servisa), besplatna dnevna kvota od 50.000 čitanja na Spark planu bi nestala u roku od nekoliko sati – i to bez ijednog stvarnog posjetitelja na stranici!

Rješenje: Prelazak na jednokratno dohvaćanje i sessionStorage caching

Prvi korak bio je gašenje požara i drastično smanjenje broja čitanja.

  1. Zamjena listenera: Uklonio sam real-time osluškivanje baze i zamijenio ga s jednokratnim getDocs pozivom. Podaci o nakitu nam ne trebaju u milisekundi u stvarnom vremenu; dovoljno je povući ih jednom pri učitavanju stranice.
  2. Klijentsko predmemoriranje (Caching): Integrirao sam sessionStorage sustav predmemoriranja. Kada klijent prvi put posjeti stranicu, povlačimo proizvode iz Firestorea, serijaliziramo ih u JSON i spremamo u preglednik s vremenom trajanja (TTL) od 30 minuta.
  3. Problem s Timestamp objektima: Firestore datume vraća kao specifične Timestamp objekte s metodama poput toMillis(). Kada te objekte spremimo u lokalni cache kao običan tekst, oni gube svoje metode, što je rušilo postojeći kod za formatiranje datuma na blogu. Napisao sam laganu funkciju koja prilikom čitanja iz cachea ručno obnavlja ove metode i vraća Timestamp prototip na noge.
  4. Smart Invalidation: Kako sestra ne bi morala čekati 30 minuta da vidi unesene promjene u trgovini, ugradio sam invalidaciju cachea izravno u administratorske akcije. Svaki put kad ona doda, izmijeni ili obriše proizvod u admin panelu, aplikacija automatski briše ključeve iz klijentovog cachea i prisilno povlači svježe podatke, dok obični kupci i dalje koriste brzi lokalni cache.

Dinamična blog sekcija


2. Druga zečja rupa: Base64 i zašto slike ne pripadaju u NoSQL

Sljedeći veliki arhitektonski problem bio je način na koji su se slike spremale. Kako bih na brzinu izbjegao postavljanje Firebase Storagea u ranoj fazi i isporučio prvu verziju što prije, u admin panelu sam koristio sučelje koje je slike pretvaralo u Base64 tekstualne stringove i spremalo ih izravno u Firestore dokumente unutar kolekcije proizvoda.

Firestore dokumenti imaju strogo ograničenje veličine od 1 MB. Kad bi sestra prenijela nekoliko slika za jedan komad nakita kako bi prikazala sve predivne morske detalje pod različitim kutovima svjetla, ti ogromni tekstualni stringovi bi brzo prešli taj limit. Osim toga, preuzimanje dokumenata koji u sebi sadrže megabajte tekstualnih slika gušilo je mobilne uređaje kupaca na slabijoj vezi i nemilosrdno trošilo mrežni promet baze podataka.

Prikaz detalja proizvoda

Rješenje: Prelazak na Firebase Storage

Baza mora služiti isključivo za metapodatke (naziv, opis, cijena, zaliha). Slike moraju ići u namjenski Object Storage.

  1. Aktiviran je Firebase Storage na besplatnom planu.
  2. Definirao sam stroga sigurnosna pravila (Security Rules) u konzoli kako bih osigurao da javnost može samo čitati slike proizvoda, dok je pisanje i brisanje dopušteno isključivo prijavljenim administratorima.
  3. Integrirao sam Storage SDK u klijentsku konfiguraciju i preusmjerio funkcije za upload da šalju binarne datoteke izravno na Storage, dok se u bazu zapisuje samo kratki, brzi URL do slike.

3. Postupak sigurne migracije bez gubitka podataka

Imali smo 51 proizvod i novosti u produkcijskoj bazi, većinom natrpane teškim Base64 tekstualnim zapisima. Kako bih riješio ovaj problem bez gubitka Goginih unesenih artikala, postupak sam podijelio u tri inženjerska koraka.

Korak A: Izolacija u lokalnom emulatoru

Kako ne bih testirao migraciju na živoj produkciji i time riskirao brisanje podataka ili ponovno trošenje dnevnih kvota, konfigurirao sam lokalne Firebase Emulatore za Firestore i Authentication.

Instalirao sam Javu 21 (što je preduvjet za pokretanje emulatora) i podesio lokalni konfiguracijski profil. Zatim sam napisao Node.js sinkronizacijsku skriptu koja je povukla stvarne podatke iz produkcije, očistila formate datuma kako ne bi bilo konflikata između različitih SDK paketa i sve to zapisala u lokalni emulator. Time sam dobio potpuno siguran pješčanik s kopijom stvarnih proizvoda na kojem sam mogao eksperimentirati bez straha.

Korak B: Klijentski migracijski alat ugrađen u Admin Panel

Umjesto pisanja komplicirane terminalske skripte s diska koja bi zahtijevala čuvanje osjetljivih Google Cloud privatnih ključeva na mom računalu, odlučio sam migraciju izvesti izravno kroz administratorsko sučelje u pregledniku!

U klijentski kod sam ugradio automatiziranu funkciju za migraciju slika i postavio je kao novu sekciju „Održavanje baze” unutar Admin panela (pod tabom Postavke).

Uređeno administratorsko sučelje za unos novih proizvoda s podrškom za više slika i specifikacije

Kada administrator pritisne gumb, logika u pregledniku radi sljedeće:

  1. Prolazi kroz sve proizvode u bazi i traži one koji sadrže slike u Base64 formatu.
  2. Uzima tekstualni Base64 string i pretvara ga natrag u binarni Blob format.
  3. Šalje taj Blob na pravi Firebase Storage s ispravnim metapodacima za JPEG sliku.
  4. Dohvaća novogenerirani javni URL.
  5. Ažurira Firestore dokument s novim nizom URL-ova slika i potpuno briše stara, teška Base64 polja kako bi očistio dokument.
  6. Između svakog proizvoda primjenjuje se throttling (pauza od 300ms) kako se klijentska veza i preglednik ne bi zagušili.

Korak C: Izvršavanje migracije i stabilizacija

Prijavio sam se u Admin Panel na produkciji, otišao pod "Održavanje baze" i pokrenuo migracijski alat. Kroz sučelje sam mogao pratiti kako preglednik uspješno prebacuje sliku po sliku na Storage i čisti Firestore dokumente. Cijeli proces je prošao besprijekorno, očistivši bazu od megabajta tekstualnog balasta u manje od minute.


4. Finalni korak: Nova stabilna baza i migracija na Cloudflare Pages

Kako bismo u potpunosti riješili sve zaostatke iz prošlosti, napravili smo još dva važna koraka:

  1. Migracija baze: Cijelu bazu podataka smo uspješno migrirali iz starog ai-studio projekta u novi, namjenski i izolirani Firestore projekt goginnakit koristeći Firestore Lite API. Time smo prekinuli sve skrivene pozadinske procese i osigurali stabilan rad.
  2. Hosting na Cloudflare Pages: Cijeli frontend je migriran s Vercela na Cloudflare Pages. Cloudflare pruža nevjerojatnu brzinu isporuke statičkih datoteka kroz svoju globalnu mrežu i idealan je za Vite/React aplikacije s komercijalnom licencom u besplatnom paketu.

Rezultati u brojkama

Nakon što smo uveli klijentski sessionStorage cache i prebacili sve slike s Base64 na Firebase Storage, Lighthouse ocjene su pokazale drastičnu promjenu:

  • Ukupna veličina učitanog koda stranice: pala je s više megabajta na samo 320 KB pri prvom učitavanju.
  • Lighthouse Performance Score (Mobilni uređaji): čvrstih 99/100.
  • Brzina učitavanja (FCP): pala je na ispod 200 milisekundi.
  • Ušteda kvota: smanjili smo broj dnevnih čitanja Firestore baze za više od 95%, čime je webshop u potpunosti osiguran od prekoračenja besplatnih limita.

Projekt je uspješno stabiliziran na produkciji i spreman za daljnje faze razvoja (Stripe plaćanje i Resend email obavijesti).

Inženjerski inat se ponovno isplatio. Izbjegli smo napuhane platforme i izgradili sustav koji leti, usput naučivši važne lekcije o klijentskom keširanju i migraciji NoSQL struktura podataka. Posjetite njezin službeni webshop na goginnakit.sdenis-vr.workers.dev i uvjerite se sami u brzinu i eleganciju! Webshop je aktivan, siguran i spreman za daljnju plovidbu! Warp speed ahead! 🖖

Pravila Privatnosti

1. Opće informacije

Dobrodošli na denissakac.com (u daljnjem tekstu "web stranica"), moj osobni blog i portfolio. Cijenim vašu privatnost i posvećen sam zaštiti osobnih podataka koje dijelite sa mnom tijekom posjeta ovoj web stranici.

2. Voditelj obrade podataka

Voditelj obrade vaših osobnih podataka je Denis Sakač. Ako imate bilo kakvih pitanja vezanih uz ova Pravila privatnosti, možete me kontaktirati putem e-pošte na: denis@denissakac.com ili sdenis.vr@gmail.com.

3. Koje podatke prikupljamo i zašto?

Ova web stranica je optimizirana za brzinu, privatnost i minimalizam. Podatke prikupljamo na sljedeće načine:

  • Tehnički dnevnici poslužitelja: Kao i većina web stranica, naš poslužitelj automatski prikuplja standardne logove (npr. IP adresa, vrsta preglednika, vrijeme posjeta) isključivo u svrhu osiguranja stabilnosti i sigurnosti sustava.
  • Kolačići (Cookies): Koristimo isključivo nužne tehničke kolačiće i lokalnu pohranu (Local Storage) u vašem pregledniku kako bismo zapamtili vaše postavke (npr. jeste li prihvatili pravila o kolačićima). Ne koristimo kolačiće za praćenje trećih strana ili oglašavanje.

4. Kolačići (Cookies) i Local Storage

Kolačići su male tekstualne datoteke koje se pohranjuju na vašem uređaju. Koristimo sljedeće podatke u vašem pregledniku:

  • denissakac_cookie_consent: Bilježi vaš odabir u prozoru za kolačiće (vrijednost "accepted" ili "declined") kako vam se prozor ne bi ponovno prikazivao pri svakom posjetu. Rok trajanja ovog zapisa je 365 dana ili dok ručno ne obrišete cache.

5. Dijeljenje podataka s trećim stranama

Vaši osobni podaci se nikada ne prodaju, ne iznajmljuju niti dijele s trećim stranama u marketinške svrhe. Podaci se mogu podijeliti isključivo ako je to zakonska obveza ili nužno za sigurnost sustava.

6. Vaša prava

Sukladno Općoj uredbi o zaštiti podataka (GDPR), imate sljedeća prava:

  • Pravo na pristup informacijama o tome koje vaše podatke obrađujemo.
  • Pravo na ispravak ili brisanje osobnih podataka.
  • Pravo na ograničenje obrade ili prigovor na obradu.
  • Pravo na povlačenje privole u bilo kojem trenutku (npr. brisanjem kolačića u pregledniku).

7. Promjene pravila privatnosti

Zadržavam pravo izmjene ovih Pravila privatnosti u bilo kojem trenutku kako bi odražavala promjene u zakonodavstvu ili funkcionalnosti web stranice. Sve izmjene bit će pravovremeno objavljene na ovoj stranici.

Zadnje ažuriranje: svibanj 2026.