SSG עם next

אחרי שלמדנו במאמר הקודם מה זה SSR והבנו שלא מדובר בקליע כסף שפותר את כל הבעיות שלנו, נלמד על SSG שיכול להקל על כמה מהבעיות של SSR.

במאמר הקודם כתבתי על SSR והיתרונות, וגם החסרונות שלו, על פני רינדור קלאסי. בסוף כתבתי על כך שאחד החסרונות המשמעותיים של SSR הוא שאם השרת קטן וחלש או עמוס, אני לא ממש חוסך זמן רינדור והלקוח יכול לחכות זמן רב עד שהשרת יואיל בטובו לרנדר את האתר.

SSG שזה ראשי תבות של Static Site Generating פותר לי את העניין הזה כי הוא מאפשר לי – איפה שאני רוצה כמובן כי לא תמיד מתאים – ליצור תוכן סטטי שירונדר רק פעם אחת בלבד ויוגש למשתמש. אני יכול לשלב את זה עם מסד נתונים או עם cache שהוא in memory. וכן, עם next אני יכול לעשות את זה בקלות ולשלב גם בין שיטות שונות. איך? בואו וניצור אפליקצית next פשוטה כמו שיצרנו בפוסט הקודם וניצור שלושה חלקים.

דף ראשון, שהוא הקלאסי – נקרא לו classical.tsx ונשים אותו תחת pages:

import { useEffect, useState } from 'react';

const ClassicPage = () => {
  const [renderTime, setRenderTime] = useState(0);

  useEffect(() => {
    const startTime = performance.now();

    // Simulate heavy computations
    const result = performCalculations(10000);

    const endTime = performance.now();
    const timeTaken = endTime - startTime;
    setRenderTime(timeTaken);
  }, []);

  const performCalculations = (iterations: number): number => {
    let sum = 0;
    for (let i = 0; i < iterations; i++) {
      sum += Math.sqrt(i);
    }
    return sum;
  };

  return (
    <div>
      <h1>Classic Rendering Page</h1>
      <p>Render Time: {renderTime}ms</p>
    </div>
  );
};

export default ClassicPage;

אם נפעיל את האפליקציה באמצעות npm run dev ונכנס, נראה שיש פה מעט SSR. המבנה הקלאסי של ה-HTML נשמר, אבל ה-render time מאוכלס רק לאחר הטעינה – בצד הלקוח. הנה הקוד שרץ – אפשר לראות שהמספר של ה-render time חושב בצד הלקוח – מצד השרת הוא התקבל כ-0 וחושב רק אחרי שהקוד נטען לצד הלקוח.

אפליקציה מצד שמאל עם render time שיש בו ערך מספרי מחושב ומצד ימין ה-HTML שהתקבל מצד השרת שבו יש 0 - כי החישוב נעשה בצד הלקוח.

אם נרצה שהכל ירונדר בצד שרת, כלומר SSR קלאסי, נשתמש בקוד הזה. ניצור דף נוסף בשם ssr.tsx ונכניס אליו את הקוד הבא:

import { GetServerSideProps } from 'next';

interface SSRPageProps {
  renderTime: number;
}

const SSRPage = ({ renderTime }: SSRPageProps) => (
  <div>
    <h1>Server-Side Rendering Page</h1>
    <p>Render Time: {renderTime}ms</p>
  </div>
);

export const getServerSideProps: GetServerSideProps<SSRPageProps> = async () => {
  const startTime = performance.now();

  // Simulate heavy computations
  const result = performCalculations(10000);

  const endTime = performance.now();
  const timeTaken = endTime - startTime;

  return {
    props: {
      renderTime: timeTaken,
    },
  };
};

const performCalculations = (iterations: number): number => {
  let sum = 0;
  for (let i = 0; i < iterations; i++) {
    sum += Math.sqrt(i);
  }
  return sum;
};

export default SSRPage;

מילת הקסם פה היא GetServerSideProps שמאותתת ל-next שמדובר בקומפוננטה שאותה אנו רוצים לרנדר בצד השרת. זה ריאקט כמו כל ריאקט אחר, אבל מרונדר בצד השרת. עברנו על זה בפוסט הקודם אבל הנה הקומפוננטה הזו – מצד שמאל יש את הערך של ה-render time ואנחנו רואים שהוא מגיע מה-HTML. כלומר חושב בצד השרת.

אפליקציה מצד שמאל עם render time שיש בו ערך מספרי מחושב ומצד ימין ה-HTML שהתקבל מצד השרת שבו יש אותו ערך כי מי שחישב את כל הסיפור זה השרת.

אם אני ארפרש את הדף – אני אראה שהערך משתנה כי הוא מחושב בכל קריאה.

הבעיה שמדובר בחישוב בצד השרת. השרת עמוס? בעייתי? אנחנו בבעיה. אנחנו כן יכולים לבצע חישוב מראש ולטעון את הדף מראש ואז לשלוח אותו למשתמש. זה שימושי במקומות מסוימים. כמובן שזה מעמיס הרבה פחות על השרת (או בכלל לא, בתצורות ענן מסוימות) ומבחינת ביצועים זה מנצח. והנה אנחנו הגענו לזה.

מימוש SSG

איך ממשים SSG? בדומה ל-SSR, אך עם פקודת GetStaticProps שאומרת ל-next שאת הקומפוננטה הזו מג׳נרטים כתוכן סטטי בלבד. הנה דוגמה לדף שאני שומר כ ssg.tsx

import { GetStaticProps } from 'next';

interface SSGPageProps {
  renderTime: number;
}

const SSGPage = ({ renderTime }: SSGPageProps) => (
  <div>
    <h1>Static Site Generation Page</h1>
    <p>Render Time: {renderTime}ms</p>
  </div>
);

export const getStaticProps: GetStaticProps<SSGPageProps> = async () => {
  const startTime = performance.now();

  // Simulate heavy computations
  const result = performCalculations(10000);

  const endTime = performance.now();
  const timeTaken = endTime - startTime;

  return {
    props: {
      renderTime: timeTaken,
    },
  };
};

const performCalculations = (iterations: number): number => {
  let sum = 0;
  for (let i = 0; i < iterations; i++) {
    sum += Math.sqrt(i);
  }
  return sum;
};

export default SSGPage;
 

אבל, מעשה שטן! אם אני אשמור את הקובץ ואריץ אותו, אני אראה שבפועל הוא מתנהג בדיוק, אבל בדיוק (!!) כמו SSR. למה? כי הדפים האלו מחושבים מראש והחישוב הזה נעשה כאשר הבילד נעשה. npm run dev לא מריץ בילד והוא מצב פיתוח. כדי לראות איך SSG עובד אנחנו חייבים להריץ את הבילד ממש כאילו אנחנו בשרת אמיתי. את זה אנו עושים עם הפקודה

npm run build && npm run start 

זה מבצע בילד ומריץ את השרת שמגיש את הקבצים הסטטיים אבל גם את השרת שמבצע את ה-SSR. אם אני אכנס לדף ה-SSG שלי, אני אראה שהוא די דומה ל SSR. אבל אם אני ארפרש שוב ושוב ושוב הערך יישאר אותו דבר וגם הכל יתבצע הרבה יותר מהר. אם אני אריץ את זה עם קוד בדיקה למשל, אני אגלה שזמן הטעינה החציוני של ה-SSG נמוך יותר בחצי!

node measure.js                    ok
SSR Median Loading Time: 7.981788 milliseconds
SSG Median Loading Time: 3.9047795 milliseconds

זה מגניב במיוחד. כי בסיכומו של עניין, באפליקציה אחת אני יכול לייצר מספר דפים – דף אחד שהוא עם רינדור קלאסי, דף אחר שהוא עם SSR ודף אחר שהוא SSG. הכל לפי העניין והצרכים של האפליקציה. כשאני מתכנן אפליקציה – אני יכול לעשות את זה בקלות.

דיפלוימנט

מבחינת דיפלוימנט, גם vercel וגם amplify יודעות להתמודד יפה עם אפליקציה שיש לה SSR, SSG וגם רינדור יותר קלאסי. כשאני עושה דיפלוי, next תדע להגיש כל תוכן בהתאם למה שהגדרתי לו.

Incremental Static Regeneration

אבל, רגע – אם אני רוצה מדי פעם לרפרש את ה-cache הזה? אני יכול לבצע בילד בכל פעם אבל… בואו ונגיד שלפעמים אין לי רצון לעשות את זה. אני רוצה להגדיר את ה-cache ואולי גם לעבוד מול מסד נתונים זה או אחר על מנת לשמור cache לכל משתמש? במקרה הזה אני צריך להשתמש בשיטה היברידית שנקראת ISR – ראשי תבות של Incremental Static Regeneration. כרגע זה נתמך אך ורק בשרתים של vercel. נכון לשורות אלו, אמזון לא תומכת ב-ISR ואם אני רוצה לאחסן שם, אפשר לעשות cache בדרכים אחרות, אבל מעט פחות נוחות.

מימוש ISR

איך אני עושה ISR? אני פשוט מעביר revalidate ל-getStaticProps – כתוב את זה בדוקומנטציה הנהדרת של next. אני אדגים באמצעות in memory cache. כלומר קאש שנשמר בזכרון ולא במסד נתונים זה או אחר.

import { GetStaticProps } from 'next';

interface SSGPageProps {
  renderTime: number;
  cacheTime: number;
}

const cache: { [key: string]: { data: any; timestamp: number } } = {};

const SSGPage = ({ renderTime, cacheTime }: SSGPageProps) => (
  <div>
    <h1>Static Site Generation Page</h1>
    <p>Render Time: {renderTime}ms</p>
    <p>Cache Time: {formatCacheTime(cacheTime)}</p>
  </div>
);

export const getStaticProps: GetStaticProps<SSGPageProps> = async () => {
  const cacheKey = 'ssgPage';

  // Check if data is available in cache
  const cachedData = cache[cacheKey];

  if (cachedData && Date.now() - cachedData.timestamp < 60000) {
    return {
      props: {
        renderTime: cachedData.data.renderTime,
        cacheTime: cachedData.timestamp,
      },
      revalidate: 60, // Re-generate the page every 60 seconds
    };
  }

  const startTime = performance.now();

  // Simulate heavy computations
  const result = performCalculations(10000);

  const endTime = performance.now();
  const timeTaken = endTime - startTime;

  // Save data to cache
  const timestamp = Date.now();
  cache[cacheKey] = {
    data: {
      renderTime: timeTaken,
    },
    timestamp,
  };

  return {
    props: {
      renderTime: timeTaken,
      cacheTime: timestamp,
    },
    revalidate: 60, // Re-generate the page every 60 seconds
  };
};

const performCalculations = (iterations: number): number => {
  let sum = 0;
  for (let i = 0; i < iterations; i++) {
    sum += Math.sqrt(i);
  }
  return sum;
};

const formatCacheTime = (timestamp: number): string => {
  const cacheDate = new Date(timestamp);
  return cacheDate.toLocaleString();
};

export default SSGPage;

אם אני אריץ באמצעות npx run dev, אני אראה שאכן ה-cache מתרפרש כל שישים שניות.

סיכום

המון ראשי תיבות שמייצגים ארבע שיטות לטעינה – SSR, SSG ו-SRI. כולן טקטיקות לבניית אפליקצית צד לקוח שנעזרת בשרת. אפשר לשלב את כל הטכניקות (למעט SRI, שלא עובדת באמזון למשל) בקלות ובפשטות גדולה באפליקציה שלכם וכאשר מתכננים אפליקציה או אתר המבוסס על next, צריך ואפשר לשקול איזה דף ואיזה קומפוננטה עובדת – יש כאלו שנרצה שיעבדו בצד הלקוח. יש כאלו שירונדרו בצד השרת, יש כאלו שירונדרו מראש כקבצים סטטיים ויש כאלו שיעבדו בשיטה היברידית עם cache שמתנקה מדי פעם. הכל תלוי בכם – מה שחשוב הוא להכיר את הסיפור הזה.

פוסטים נוספים שכדאי לקרוא

פיתוח ב-JavaScript

Axios interceptors

תכנון נכון של קריאות AJAX באפליקציה ריאקטית וניהול השגיאות או ההצלחות עם פיצ׳ר נחמד של axios

פתרונות ומאמרים על פיתוח אינטרנט

SSG עם next

אחרי שלמדנו במאמר הקודם מה זה SSR והבנו שלא מדובר בקליע כסף שפותר את כל הבעיות שלנו, נלמד על SSG שיכול להקל על כמה מהבעיות של SSR.

גלילה לראש העמוד