แอปพลิเคชันจำนวนมากแสดงเนื้อหาเดียวกันแก่ผู้ใช้ทุกคนในการโหลดหน้าเว็บครั้งแรก เช่น เว็บไซต์ข่าวอาจแสดงเรื่องราวล่าสุด หรือเว็บไซต์อีคอมเมิร์ซอาจแสดงสินค้าขายดี
หากเนื้อหานี้แสดงจาก Cloud Firestore ผู้ใช้แต่ละรายจะส่งคำค้นหาใหม่สำหรับผลลัพธ์เดียวกันเมื่อโหลดแอปพลิเคชัน เนื่องจากผลลัพธ์เหล่านี้ไม่ได้แคชระหว่างผู้ใช้ แอปพลิเคชันจึงทำงานช้าลงและมีค่าใช้จ่ายมากกว่าที่ควรจะเป็น
โซลูชัน: แพ็กเกจ
แพ็กเกจ Cloud Firestore ช่วยให้คุณรวบรวมแพ็กเกจข้อมูลจากผลการค้นหาทั่วไปในแบ็กเอนด์โดยใช้ Firebase Admin SDK และแสดง Blob ที่คำนวณไว้ล่วงหน้าเหล่านี้ซึ่งแคชไว้ใน CDN ซึ่งจะช่วยให้ผู้ใช้ได้รับประสบการณ์การโหลดครั้งแรกที่รวดเร็วยิ่งขึ้นและลดค่าใช้จ่ายในการค้นหา Cloud Firestore
ในคู่มือนี้ เราจะใช้ Cloud Functions เพื่อสร้างแพ็กเกจและใช้ Firebase Hosting เพื่อแคชและแสดงเนื้อหาแพ็กเกจแบบไดนามิก ดูข้อมูลเพิ่มเติมเกี่ยวกับแพ็กเกจได้ในคู่มือ
ก่อนอื่น ให้สร้างฟังก์ชัน HTTP สาธารณะแบบง่ายเพื่อค้นหา "เรื่องราว" ล่าสุด 50 รายการและแสดงผลลัพธ์เป็นแพ็กเกจ
Node.js
exports.createBundle = functions.https.onRequest(async (request, response) => { // Query the 50 latest stories const latestStories = await db.collection('stories') .orderBy('timestamp', 'desc') .limit(50) .get(); // Build the bundle from the query results const bundleBuffer = db.bundle('latest-stories') .add('latest-stories-query', latestStories) .build(); // Cache the response for up to 5 minutes; // see https://firebase.google.com/docs/hosting/manage-cache response.set('Cache-Control', 'public, max-age=300, s-maxage=600'); response.end(bundleBuffer); });
Java
package com.example; import com.google.auth.oauth2.GoogleCredentials; import com.google.cloud.firestore.Firestore; import com.google.cloud.firestore.FirestoreBundle; import com.google.cloud.firestore.Query.Direction; import com.google.cloud.firestore.QuerySnapshot; import com.google.cloud.functions.HttpFunction; import com.google.cloud.functions.HttpRequest; import com.google.cloud.functions.HttpResponse; import com.google.firebase.FirebaseApp; import com.google.firebase.FirebaseOptions; import com.google.firebase.cloud.FirestoreClient; import java.io.BufferedWriter; import java.io.IOException; public class ExampleFunction implements HttpFunction { public static FirebaseApp initializeFirebase() throws IOException { if (FirebaseApp.getApps().isEmpty()) { FirebaseOptions options = FirebaseOptions.builder() .setCredentials(GoogleCredentials.getApplicationDefault()) .setProjectId("YOUR-PROJECT-ID") .build(); FirebaseApp.initializeApp(options); } return FirebaseApp.getInstance(); } @Override public void service(HttpRequest request, HttpResponse response) throws Exception { // Get a Firestore instance FirebaseApp app = initializeFirebase(); Firestore db = FirestoreClient.getFirestore(app); // Query the 50 latest stories QuerySnapshot latestStories = db.collection("stories") .orderBy("timestamp", Direction.DESCENDING) .limit(50) .get() .get(); // Build the bundle from the query results FirestoreBundle bundle = db.bundleBuilder("latest-stores") .add("latest-stories-query", latestStories) .build(); // Cache the response for up to 5 minutes // see https://firebase.google.com/docs/hosting/manage-cache response.appendHeader("Cache-Control", "public, max-age=300, s-maxage=600"); // Write the bundle to the HTTP response BufferedWriter writer = response.getWriter(); writer.write(new String(bundle.toByteBuffer().array())); } }
จากนั้นกำหนดค่าโฮสติ้งของ Firebase เพื่อแสดงและแคช Cloud Function นี้โดยแก้ไข firebase.json เมื่อใช้การกำหนดค่านี้ CDN ของ Firebase Hosting จะแสดงเนื้อหาแพ็กเกจตามการตั้งค่าแคชที่กำหนดโดย Cloud Function เมื่อแคชหมดอายุ ระบบจะรีเฟรชเนื้อหาโดยทริกเกอร์ฟังก์ชันอีกครั้ง
firebase.json
{
"hosting": {
// ...
"rewrites": [{
"source": "/createBundle",
"function": "createBundle"
}]
},
// ...
}
สุดท้าย ในเว็บแอปพลิเคชัน ให้ดึงข้อมูลเนื้อหาที่รวมไว้จาก CDN และโหลดลงใน Firestore SDK
// If you are using module bundlers.
import firebase from "firebase/app";
import "firebase/firestore";
import "firebase/firestore/bundle" // This line enables bundle loading as a side effect.
async function fetchFromBundle() {
// Fetch the bundle from Firebase Hosting, if the CDN cache is hit the 'X-Cache'
// response header will be set to 'HIT'
const resp = await fetch('/createBundle');
// Load the bundle contents into the Firestore SDK
await db.loadBundle(resp.body);
// Query the results from the cache
// Note: omitting "source: cache" will query the Firestore backend.
const query = await db.namedQuery('latest-stories-query');
const storiesSnap = await query.get({ source: 'cache' });
// Use the results
// ...
}
ค่าใช้จ่ายที่ประหยัดได้โดยประมาณ
ลองนึกถึงเว็บไซต์ข่าวที่มีผู้ใช้ 100,000 คนต่อวัน และผู้ใช้แต่ละรายโหลดเรื่องเด่น 50 เรื่องเดียวกันในการโหลดครั้งแรก หากไม่มีการแคช จะส่งผลให้มีการอ่านเอกสาร 50 x 100,000 = 5,000,000 ครั้งต่อวันจาก Cloud Firestore
ตอนนี้สมมติว่าเว็บไซต์ใช้เทคนิคข้างต้นและแคชผลการค้นหา 50 รายการเหล่านั้นเป็นเวลาสูงสุด 5 นาที ดังนั้น แทนที่จะโหลดผลการค้นหาสำหรับผู้ใช้ทุกคน ระบบจะโหลดผลการค้นหา 12 ครั้งต่อชั่วโมงพอดี ไม่ว่าจะมีผู้ใช้เข้าชมเว็บไซต์กี่คน จำนวนคำค้นหาใน Cloud Firestore ก็จะยังคงเท่าเดิม แทนที่จะเป็นการอ่านเอกสาร 5,000,000 ครั้ง หน้านี้จะใช้การอ่านเอกสาร 12 x 24 x 50 = 14,400 ครั้งต่อวัน ค่าใช้จ่ายเพิ่มเติมเล็กน้อยสำหรับโฮสติ้งของ Firebase และ Cloud Functions จะได้รับการชดเชยอย่างง่ายดายด้วยการประหยัดค่าใช้จ่าย Cloud Firestore
แม้ว่านักพัฒนาแอปจะได้รับประโยชน์จากการประหยัดค่าใช้จ่าย แต่ผู้ที่ได้รับประโยชน์สูงสุดคือผู้ใช้ การโหลดเอกสาร 50 รายการเหล่านี้จาก CDN ของโฮสติ้งของ Firebase แทนที่จะโหลดจาก Cloud Firestore โดยตรงจะช่วยลดเวลาในการโหลดเนื้อหาของหน้าเว็บได้ 100-200 มิลลิวินาทีหรือมากกว่านั้นได้อย่างง่ายดาย การศึกษาหลายชิ้นแสดงให้เห็นว่าหน้าเว็บที่โหลดเร็วทำให้ผู้ใช้มีความสุขมากขึ้น