• בלוג
  • בואו נשכנע את ג'מיני לשים לב לבעיה בקוד

בואו נשכנע את ג'מיני לשים לב לבעיה בקוד

13/04/2025

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

1. טייק 1 - Code Review

הקוד שמעניין אותי הוא הקומפוננטה CommentTextarea שמוגדרת בקובץ:

https://github.com/antiwork/gumroad/blob/main/app/javascript/components/Post/PostCommentsSection.tsx

יש שם שימוש לא אידאלי ב useEffect שפוגע בביצועים ומשנה את מאפיין הגובה כל פעם שהתוכן משתנה לפי גודל גובה התוכן. בדרך כלל עדכוני גובה כאלה צריכים להיות ב useLayoutEffect, ויותר מזה כשמדובר ב textarea יש מאפיין CSS בשם field-sizing שגורם ל textarea לגדול אוטומטית לפי גודל התוכן. הייתי רוצה ש AI יזהה את הבעיה ויציע את התיקון הנכון בעזרת CSS, וגם ישלח אותי לקרוא על הקשר בין useEffect לביצועים ולמה לא כדאי להשתמש במבנה זה כדי שאלמד לפעם הבאה.

הניסיון הראשון שלי היה להדביק את כל הקוד לג'מיני ולבקש Code Review. זה היה הפרומפט:

const CommentTextarea = ({
  children,
  showAvatar = true,
  ...props
}: React.ComponentProps<"textarea"> & { showAvatar?: boolean }) => {
  const appDomain = useAppDomain();
  const { purchase_id } = useCommentsMetadata();
  const loggedInUser = useLoggedInUser();
  const ref = React.useRef<HTMLTextAreaElement | null>(null);
  React.useEffect(() => {
    if (!ref.current) return;

    ref.current.style.height = "inherit";
    ref.current.style.height = `${ref.current.scrollHeight}px`;
  }, [props.value]);

  return (
    <div className={cx({ comment: showAvatar })} style={showAvatar ? {} : { display: "grid", gap: "var(--spacer-3)" }}>
      {showAvatar ? (
        <img className="user-avatar" alt="Current user avatar" src={loggedInUser?.avatarUrl ?? defaultUserAvatar} />
      ) : null}
      {loggedInUser || purchase_id ? (
        <textarea ref={ref} rows={1} placeholder="Write a comment" {...props} />
      ) : (
        <div>
          <a href={Routes.login_url({ host: appDomain })}>Log in</a> or{" "}
          <a href={Routes.signup_url({ host: appDomain })}>Register</a> to join the conversation
        </div>
      )}
      {loggedInUser != null || purchase_id != null ? (
        <div style={{ display: "grid", justifyContent: "end", gap: "var(--spacer-3)", gridAutoFlow: "column" }}>
          {children}
        </div>
      ) : null}
    </div>
  );
};

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

Auto-Resizing useEffect:

Efficiency: The effect runs on every change to props.value (i.e., every keystroke). While often fine, for very complex components or extremely rapid typing, this could potentially lead to slight performance hiccups (though usually negligible). Debouncing is an option if performance becomes an issue, but adds complexity.

Initial Render: Ensure the initial height calculation works as expected when the component mounts and props.value might be empty or undefined. The rows={1} attribute helps set a minimum size initially.

CSS box-sizing: The accuracy of scrollHeight can be affected by CSS properties like padding and box-sizing. Ensure your CSS accounts for this (box-sizing: border-box is recommended).

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

2. טייק 2 - קומפוננטה קטנה ופוקוס על ביצועים

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

code review the following component with focus on performance:

const ResizingTextArea = ({
  value,
}) => {
  const ref = useRef(null);
    React.useEffect(() => {
    if (!ref.current) return;

    ref.current.style.height = "inherit";
    ref.current.style.height = `${ref.current.scrollHeight}px`;
  }, [value]);

  return (
    <textarea ref={ref} value={value}></textarea>
  )
}

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

import React, { useState, useRef, useLayoutEffect, useCallback } from 'react';

// Simple debounce function (or use one from lodash/underscore)
const debounce = (func, wait) => {
  let timeout;
  return function executedFunction(...args) {
    const later = () => {
      clearTimeout(timeout);
      func(...args);
    };
    clearTimeout(timeout);
    timeout = setTimeout(later, wait);
  };
};

const ResizingTextArea = ({ value, onChange, style, ...props }) => {
  const ref = useRef(null);

  // Memoize the resize function using useCallback
  const resize = useCallback(() => {
    if (ref.current) {
      // Reset height - necessary to shrink correctly
      ref.current.style.height = 'inherit';
      // Set height based on scrollHeight
      ref.current.style.height = `${ref.current.scrollHeight}px`;
    }
  }, []); // No dependencies needed if it only accesses ref.current

  // Debounce the resize function
  // Adjust the delay (e.g., 150ms) as needed
  const debouncedResize = useCallback(debounce(resize, 150), [resize]);

  // Resize on value change (debounced)
  useLayoutEffect(() => {
    // Call debounced resize. It might run immediately the first time
    // or after a delay on subsequent changes.
    debouncedResize();
    // We only want this effect to run when `value` changes or debouncedResize changes.
  }, [value, debouncedResize]);

  // Resize on window resize (debounced)
  useLayoutEffect(() => {
    // Initial resize on mount
    resize();

    window.addEventListener('resize', debouncedResize);

    // Cleanup listener on unmount
    return () => {
      window.removeEventListener('resize', debouncedResize);
      // Optional: Cancel any pending debounced calls on unmount
      // If using lodash debounce, it often has a .cancel() method.
      // For the simple debounce above, clearing the timeout isn't
      // easily exposed without modifying the debounce function itself.
      // However, the check `if (ref.current)` inside resize handles this.
    };
  }, [resize, debouncedResize]); // Rerun if resize functions change (shouldn't often)


  // --- Enhancements for usability ---
  // Allow parent to control the textarea fully
  const handleChange = (event) => {
    // Trigger the resize immediately on change for responsiveness *before* state update
    // Note: This might cause a slight flicker if the debounced one runs later.
    // Alternatively, rely solely on the debounced effect driven by `value` prop change.
    // For simplicity and relying on the debounced effect: comment the next line out.
    // resize(); // Optional: Immediate resize attempt

    if (onChange) {
      onChange(event);
    }
  };

  return (
    <textarea
      ref={ref}
      value={value}
      onChange={handleChange}
      style={{
        overflow: 'hidden', // Hide scrollbar that might briefly appear
        resize: 'none',     // Prevent manual resize corner if not desired
        ...style,           // Allow passing custom styles
      }}
      rows={1} // Start with minimum rows (optional but common)
      {...props} // Pass down other props like placeholder, name, etc.
    />
  );
};

export default ResizingTextArea;

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

3. טייק 3 - שני פרומפטים

בניסיון שלישי אני רוצה לכוון את ה AI לנושא הביצועים ולתת לו להבין שיש כמה דרכים כדי לדחוף אותו לכיוון פיתרון ב CSS. אני מתחיל עם:

Code review the following component. Focus on performance. Only explain the problematic points without suggesting fixes.

Consider the larger picture. If there is a simpler way to achieve the same goal please point me to it, but don't implement yet any fixed version.

const ResizingTextArea = ({
  value,
}) => {
  const ref = useRef(null);
    React.useEffect(() => {
    if (!ref.current) return;

    ref.current.style.height = "inherit";
    ref.current.style.height = `${ref.current.scrollHeight}px`;
  }, [value]);

  return (
    <textarea ref={ref} value={value}></textarea>
  )
}

הפעם אני מקבל הסבר של הנקודות הבעייתיות שמסתיים בפיסקה

There is a modern, potentially simpler, and more performant approach to achieve self-sizing textareas that relies primarily on CSS, minimizing or eliminating the need for JavaScript-based height calculations on every input change.

אני ממשיך את השיחה עם פרומפט נוסף וכותב:

Implement the most simple fix for the performance issue. Use inline style if you need to add CSS. Don't add new files

התוצאה הפעם:

const ResizingTextArea = ({
  value,
  // It's good practice to allow passing other props like onChange, placeholder etc.
  ...props 
}) => {

  // No more useRef or useEffect needed for resizing

  return (
    <textarea
      value={value}
      style={{
        // This CSS property instructs the browser to size the field based on its content
        fieldSizing: 'content', 
        // Optional: Prevents scrollbars from briefly appearing during resize in some browsers
        // and ensures it doesn't overflow if a max-height is also applied elsewhere.
        overflow: 'hidden', 
        // You might want to add a minHeight as well
        // minHeight: '50px', // Example minimum height
      }}
      {...props} // Spread remaining props
    />
  );
};

סוף סוף הוא קלע עם ה field-sizing. הוא הוסיף overflow שלא ביקשתי וגם את ה spread של כל הפרופס האחרים ששוב לא ביקשתי והכי גרוע אינסוף הערות שרק מקשות על ההבנה והתחזוקה, אבל אי אפשר לקבל הכל בחיים.

4. מה למדנו

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