הבלוג של ינון פרק

טיפים קצרים וחדשות למתכנתים

קבלת מידע ב Streaming היא דווקא די פשוטה

27/12/2024

ממשקי Chat מאוד אוהבים להשתמש ב Streaming כי ל AI לוקח המון זמן לייצר את התשובה. בצד הלקוח משיכת מידע משרת HTTP ב Streaming התבררה כמשימה די פשוטה אני מדביק פה את הקוד ואחריו ההסבר:

export default async function* sendQuestion(questionText: string) {
  const res = await fetch('http://localhost:11434/api/generate', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      model: 'llama3.2',
      prompt: questionText,
      stream: true,
    }),
  })

  if (!res.ok) throw new Error('Failed to connect to Ollama server')

  const reader = res.body?.getReader()
  if (!reader) throw new Error('Failed to initialize stream reader')

  while (true) {
    const { done, value } = await reader.read()
    if (done) break

    // Decode the stream data
    const chunk = new TextDecoder().decode(value)            
    const json = JSON.parse(chunk)
    yield json
  }
}

הקוד פונה לשרת של Ollama שרץ אצלי מקומית על המחשב ושולח לו שאלה. הפרמטר stream מקבל ערך אמת אבל זה לא הכרחי כי זו ברירת המחדל של פרמטר זה. אם התשובה היתה מגיעה בתור אוביקט אחד הייתי מפעיל את res.json() או res.text() כדי לקבל אותה, אבל בגלל שהתשובה מגיעה בתור זרם של אוביקטים אני קורא ל getReader על ה body, כדי לקבל ממשק קריאה. ה body הוא בעצם ReadableStream ואפשר לקרוא עליו בהרחבה בדף התיעוד:

https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream/getReader

אחרי שלקחתי את ה Reader מתחיל החלק המעייף - עלינו לקרוא מה Reader את המידע חלק אחרי חלק ולפענח כל פעם את החלק הבא בתור אוביקט JSON. בקוד הזה אני מניח שכל Chunk יכיל אוביקט JSON שלם, למרות שבעולם האמיתי ייתכן ויידרש טיפול יותר פרטני כשפיענוח של Chunk נכשל כי אולי זה חלק שמחזיק רק חלק מאוביקט ה JSON וההמשך שלו הוא ב Chunk הבא. שימו לב לשימוש ב TextDecoder שמונע בעיה דומה עבור פיענוח הטקסט, כלומר ה decode של TextDecoderיודע לחתוך את הביטים העודפים אם יש ולהדביק אותם להתחלה של ה Chunk הבא כדי שיוכל להפוך ביטים לטקסט.

הפונקציה כולה היא Generator ואפשר להשתמש בה למשל מתוך קומפוננטת vue באופן הבא:

<script setup lang="ts">
import { ref } from 'vue'
import streamChatResponse from '../streamChatResponse';
const question = ref('')
const response = ref('')
const isLoading = ref(false)
const error = ref<string | null>(null)

async function sendQuestion() {
  for await (const chunk of streamChatResponse(question.value)) {
    response.value += chunk.response;
  }
  question.value = '';
}

</script>

<template>
  <div class="chat-container">
    <form @submit.prevent="sendQuestion" class="question-form">
      <textarea
        v-model="question"
        placeholder="Ask a question..."
        :disabled="isLoading"
        class="question-input"
      />
      <button 
        type="submit" 
        :disabled="isLoading || !question.trim()"
        class="submit-button"
      >
        {{ isLoading ? 'Thinking...' : 'Send' }}
      </button>
    </form>

    <div v-if="error" class="error-message">
      {{ error }}
    </div>

    <div v-if="response" class="response-container">
      <div class="response-text">{{ response }}</div>
    </div>
  </div>
</template>

<style scoped>
.chat-container {
  max-width: 800px;
  margin: 0 auto;
  padding: 1rem;
}

.question-form {
  display: flex;
  gap: 1rem;
  margin-bottom: 1rem;
}

.question-input {
  flex: 1;
  min-height: 80px;
  padding: 0.5rem;
  border: 1px solid #ccc;
  border-radius: 4px;
  resize: vertical;
}

.submit-button {
  padding: 0.5rem 1rem;
  background-color: #4CAF50;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}

.submit-button:disabled {
  background-color: #cccccc;
  cursor: not-allowed;
}

.error-message {
  color: #dc3545;
  margin-bottom: 1rem;
}

.response-container {
  padding: 1rem;
  background-color: #f8f9fa;
  border-radius: 4px;
}

.response-text {
  white-space: pre-wrap;
}
</style>

הכלי הכי טוב

26/12/2024

בשביל לכתוב יישום ריאקט גדול בלי באגים ולהצליח להתמודד עם דרישות חדשות שמגיעות לאורך זמן בטוח צריך להבין מה זה State Management, צריך להחליט אם רוצים להשתמש ב Context כדי לשמור סטייט, אם מסתפקים בספריית ניהול סטייט קטנה כמו Valtio או שהולכים על כל הקופה ומשלבים רידאקס. אבל בתחילת הדרך או כשהיישום קטן הכלים האלה יכולים מאוד לבלבל.

וכאן השאלה - עד כמה צריך להתאמץ להבין איך רידאקס עובד כשאנחנו רק לומדים ריאקט? עד כמה חשוב להבין איך GraphQL עובד כשאנחנו רק מתחילים ללמוד על פיתוח קוד צד שרת? עד כמה חשוב "להתרגל" לשיטת עבודה מסוימת רק בגלל שהיא Best Practice ותעזור לנו בעתיד?

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

הכלי הכי טוב הוא כלי שפותר בעיה שעכשיו יש לך.

דוגמת קוד: וידוא קלט בשרת עם yup

25/12/2024

לפני כמה ימים הראיתי כאן איך לבדוק קלט בטופס צד לקוח עם yup. ראינו ש yup מגיע עם המון יכולות אבל החיבור שלו לממשק דורש עבודה וברוב המקרים אפשר לקבל תוצאה טובה יותר דרך הכלים המובנים ב HTML. מצד שני בעבודת צד שרת אנחנו מגלים כמה yup יעיל וגם המבנה האסינכרוני שלו נראה מאוד הגיוני, כי ממילא כל ה APIs ב node הם אסינכרוניים. שימו לב לקוד הבא לצד שרת עבור REST API עם Hono:

app.post('/signup', async (c) => {
  const body = await c.req.parseBody()
  try {
    await signupUserSchema.validate(body);
    // create the user and redirect
    return c.json({ ok: true });
  } catch (err) {
    throw new HTTPException(401, {message: String(err)})
  }
});

הקוד לוקח נתונים שהגיעו מבקשת POST לטופס ומוודא שהם תקינים תוך שימוש בסכימה של yup. המבנה הזה טוב כי הוא מפריד בין "מה שצריך לעשות" ל"איך צריך לעשות את זה", כלומר ברור כשמסתכלים על הקוד שיש פה וולידציה, ברור מה קורה אם הוולידציה מצליחה או אם היא נכשלת, אבל הוולידציה עצמה מבוססת על סכימה שמוגדרת במקום אחר. אם בעתיד נרצה לשנות שדות בטופס או את חוקי הוולידציה שלהם לא נצטרך לגעת בקוד של ה Endpoint.

ורק בשביל להשלים את התמונה זה קוד הסכימה מתוך הקובץ signupUserSchema:

import {object, string} from 'yup';

export default object().shape({
  email: string().required().email(),
  password: string().required().min(3),
});

וקוד הטופס:

<!DOCTYPE html>
<html>
  <body>
    <h1>Sign Up</h1>
    <form action="/signup" method="post">
      <label>
        Email
        <input type="email" name="email" required />
      </label>

      <label>
        Password
        <input type="password" name="password" required minlength="3" />
      </label>

      <input type="submit" />
    </form>
  </body>
</html>

"רק" עוד render אחד

24/12/2024

הבעיה היא לא ה"רק" עוד render אחד שהקומפוננטה שלך מוסיפה למערכת בגלל שימוש לא נכון ב useEffect.

הבעיה היא מאות ה render-ים שיתווספו בארבע השנים הקרובות כשאנשים יעתיקו את הקוד הזה לעוד מקומות וישתמשו בקומפוננטה בדרכים לא צפויות.

יש רק שתי דרכים לזוז קדימה ולשמור על מהירות לאורך זמן:

  1. אפשר לכתוב תמיד את הקוד הכי נכון (קשה מאוד).

  2. אפשר לכתוב קוד עובד ופעם בכמה ימים לנקות אותו (גם קשה מאוד).

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

איך כדאי לבנות

23/12/2024

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

  1. יש לי שרת REST API ואלה ה Endpoints שלו. מה יכולות להיות הבעיות בממשק שבחרתי? איזה דרישה או דרישות לקוח יהיה לי קשה לממש?

  2. נתונה ספריית ריאקט וזה ה API שלה. באיזה סוגי מערכות יהיה קל לשלב אותה? ואיפה יהיה קשה?

  3. נתון מבנה הטבלאות בבסיס הנתונים. למה כדאי לשים לב כשיוצרים את בסיס הנתונים? איזה פיצ'רים יהיה קל או קשה לממש עם טבלאות אלה? איזה פיצ'ר עלול לחייב אותי ל Data Migration משמעותי?

  4. נתון קוד שעושה X. איזה דרישה חדשה יכולה להגיע מהלקוח שתשבור את הקוד הזה? עם איזה דרישות לקוח יהיה מאוד קל להתמודד בעזרת הקוד הקיים?

  5. נתונים שלושה מימושים לפונקציה X, כולם מחזירים את אותה תוצאה. מה היתרונות והחסרונות של כל מימוש?

המשותף לכולן: במקום לשאול "איך עושים X" אנחנו עוברים לשאול "איך כדאי לעשות X".

דוגמת קוד: בדיקת טופס עם yup ב vue

22/12/2024

הדרך הכי קלה היום לעבוד עם טפסים היא לדלג עליהם בספריית ה SPA שלנו, ולהשתמש במאפייני ה HTML ו ValidityState של רכיבי הקלט כדי להציג הודעות למשתמשים. ועדיין לפעמים אנחנו צריכים בדיקות יותר מסובכות או שרוצים להריץ תהליך יותר מסודר סביב הנושא של בדיקות קלט בצד לקוח. במצבים כאלה נוכל להשתמש בספריה כמו yup כדי לתאר את הטופס ובקוד לעדכן את הודעות השגיאה בתוך אפליקציית vue או ריאקט. בואו נראה איך זה עובד.

המשך קריאה

אלא אם כן אתם נטפליקס

21/12/2024

כשגולש נכנס אליכם לאתר, כמה זמן הדפדפן שלו צריך לעבוד ולהריץ JavaScript לפני שהוא יכול להשתמש באתר? מסתבר שבממוצע התשובה תלויה הרבה יותר בטכנולוגיה איתה בחרתם לבנות את האתר מאשר בפיצ'רים של האתר עצמו. הלכתי ל Page Speed לבדוק 9 אתרי ריאקט ו next.js ולא הופתעתי מהתוצאות:

  1. בשביל לקרוא מאמר מהאתר של vercel צריך להריץ JavaScript במשך 2.5 שניות.

  2. דף הבית של nike העסיק את הדפדפן במשך 8.1 שניות רק בהרצת הקוד.

  3. דף הבית של solana עם הסלוגן Poweful for developers, Fast for everyone בילה "רק" 1.6 שניות בהרצת קוד.

  4. לפני שבוחרים ארוחה ב wegmans הדפדפן צריך לבלות 10 שניות בהרצת ה JavaScript שלהם.

  5. לפני שבוחרים קורס באקדמיה של קאן צריך לחכות 2.9 שניות כדי להריץ את ה JavaScript אצלם בעמוד.

  6. לפני שאפשר לקרוא עדכונים ברדיט נקדיש 2.3 שניות להרצת JavaScript.

  7. בקורסרה נבלה 3.8 שניות בהרצת ה JavaScript לפני שנוכל להיכנס לקורסים.

  8. יודמי טיפה יותר טובים עם 3.1 שניות זמן ריצה של JavaScript בכניסה לעמוד.

צריך להגיד - משתמשים לא רוצים לבלות זמן בהמתנה ל JavaScript שירוץ. אם אפשר היה לבנות את האתר בצורה שתתן את אותה פונקציונאליות אבל בלי 10 שניות של זמן ריצה של JavaScript כולם היו שמחים יותר. מה שמשותף לכל האתרים ברשימה הוא הבחירה ב React, חלקם ישירות וחלקם גם עם next.js. כן יהיה מעניין לבדוק גם פריימוורקים אחרים אבל לא זאת הנקודה כאן. בואו נסתכל על עוד שני אתרים מעניינים:

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

אתר שני הוא נטפליקס, וידעתם שהוא יבוא. נטפליקס יודעים מה הם עושים ומוכנים להשקיע בתהליכי פיתוח, אופטימיזציות ומה לא. עמוד ה jobs שלהם בכתובת jobs.netflix.com משתמש בריאקט בצורה מאוד אפקטיבית ומבלה רק 0.6 שניות בהרצת JavaScript כשהדף נטען. אגף גם אתר התיעוד של ריאקט מאוד יעיל ומגיע לתוצאות דומות ואף טובות יותר.

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

ה fix השני מיותר

20/12/2024

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

כשאני מתחיל להסתכל על קוד אחד המסכים הראשונים שאני פותח הוא הסיטוריית הקומיטים. המטרה היא להבין קצת את המחשבה של מי שכותב את הקוד, לפני שאני צולל לבעיות עצמן ולפיתרונות שנבחרו. בריפו של konfig, הסטארט-אפ של אותו דילן, תופעה אחת בלטה מאוד במסך הקומיטים. הנה כמה דוגמאות:

f1ed4259c fix
75e825f62 fix
c388772ec remove unnecessary env group
1cc57ce33 fix sizing of logo
d26a25c47 fix sizing
f510e4f5d update
89007aad9 update
0d9701a14 updates to workflow

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

יצרתי ריפו ריק ובתוכו רק את שלושת הקומיטים כך שהלוג הוא:

41a1948 fix
0a197d4 fix
e7572cf remove unnecessary env group
daf174b initial commit

בגלל שלא דחפתי את השינויים לשום מקום, כל מה שצריך בשביל לשנות את שלושת הודעות הקומיט הוא "לחזור אחורה" שלושה קומיטים ולעשות קומיט מחדש:

$ git reset HEAD~3
Unstaged changes after reset:
M       textfile.txt

$ git add .
$ git commit -m 'remove unnecessary env group'

הפרויקט כעת נמצא באותו מצב של קומיט 41a1948 אבל הלוג הוא:

622a0da remove unnecessary env group
daf174b initial commit

ומי שיסתכל בלוג יראה בתוך קומיט 622a0da את כל השינויים של כל שלושת הקומיטים המקוריים.

נ.ב. יש מערכות שמכריחות אותך לעשות Push כדי להפעיל CI Pipeline. שינוי קומיטים אחרי דחיפה הוא תהליך הרבה פחות ידידותי ויכול לבלבל מפתחים אחרים שכבר משכו את הקומיטים הקודמים. במצבים כאלה אני אוהב ליצור ענף נפרד לצורך הפעלת ה CI ואז אחרי שסיימתי לבדוק אני ממזג את הענף ומוחק את הקומיטים המיותרים עם ריבייס.

לא צריך לבחור שניים

19/12/2024

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

לפעמים המשולש הזה עוזר לנו להבין את המציאות.

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

"מצד אחד אני רוצה לעבוד על פרויקטים מעניינים, אבל מצד שני רוצה להרוויח יותר כסף".

"מצד אחד אני רוצה לבנות מוצר יותר מהר, אבל מצד שני לא רוצה להתפשר על איכות הקוד".

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

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

שלושה טריקים ששיפרו לי משמעותית את ציון הביצועים ב Page Speed

18/12/2024

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

אלה שלושת הטריקים שיישמתי כדי לשפר משמעותית את ציון ה Page Speed שעכשיו עומד על 94 לדף של פוסט מהבלוג:

המשך קריאה