วิธีหาโค้ด React ที่ไม่มีใครใช้ก่อนเริ่ม refactor

ก่อนแยกไฟล์ใหญ่ ให้เช็กก่อนว่ามีหน้าจริงของแอปเรียกถึงไฟล์นั้นไหม โดยไล่ import จาก route, layout และ registry ไม่นับ test เป็นคนเรียก ใช้ git grep หรือ knip --production แล้วลบโค้ดที่ไม่มีใครใช้พร้อม test ของโค้ดนั้น ดีกว่าเสียเวลา refactor โค้ดที่ตายแล้ว

เผยแพร่
เวลาอ่าน
5 นาที

หมวด 4 · Refactor ให้ปลอดภัยบทที่ 14 จาก 14

สารบัญบทเรียน บทที่ 14 จาก 14
ในบทนี้ 9 หัวข้อ
  1. โค้ดที่ไม่มีใครใช้มีต้นทุนมากกว่าที่คิด
  2. อาการของโค้ดที่น่าจะตายแล้ว
  3. เช็กด้วย git grep ก่อน
  4. ใช้ knip --production ไล่ทั้งโปรเจกต์
  5. ลบ props ที่ตายแล้วจะเผยให้เห็น module ที่ตายตามไปด้วย
  6. ลบให้ปลอดภัย
  7. กันโค้ดตายใหม่ด้วย knip ใน CI
  8. เมื่อไหร่ไม่ควรรีบลบ
  9. จากงานจริง

ก่อนแยกไฟล์ที่ยาวเป็นพันบรรทัด มีคำถามหนึ่งที่ควรถามก่อนเสมอ คือ ไฟล์นี้ยังมีหน้าจริงของแอปเรียกถึงอยู่ไหม โปรเจกต์ที่โตมาหลายปีมีโค้ดของ feature ที่เลิกใช้ไปแล้วค้างอยู่เสมอ ยังผ่าน typecheck ยังมี test เขียว แต่ไม่มีหน้าไหนเปิดถึง

กฎของบทนี้คือ ก่อน refactor ไฟล์ไหน ให้พิสูจน์ก่อนว่ายังมีของจริงเรียกใช้ โดยไล่ import จากจุดเริ่มของแอป เช่น route, layout, plugin หรือ registry และไม่นับ test เป็นคนเรียก ถ้าไม่มีใครเรียก ให้ลบพร้อม test ของไฟล์นั้น ไม่ต้อง refactor

โค้ดที่ไม่มีใครใช้มีต้นทุนมากกว่าที่คิด

โค้ดที่ตายแล้วไม่ได้นอนเฉย ๆ

  • คนอ่านต้องเสียเวลาทำความเข้าใจ เปิดไฟล์มาเจอ props สิบตัวที่ไม่รู้ว่าใช้ที่ไหน ต้องไล่ทุกตัวก่อนจะกล้าแก้อะไร
  • test ของโค้ดนี้ยังรันทุกครั้ง CI ช้าลง และพังได้เมื่อโค้ดข้าง ๆ เปลี่ยน ทั้งที่ไม่มีผลกับผู้ใช้เลย
  • เครื่องมือตรวจนับเป็นงานด้วย สคริปต์ที่หาไฟล์ใหญ่เกินเกณฑ์จะชี้มาที่ไฟล์นี้ แล้วเราก็เสียเวลาแยกไฟล์ที่ควรลบ
  • AI agent ก็อ่านโค้ดนี้ด้วย agent ที่หาตัวอย่างในโปรเจกต์อาจเอา pattern จากโค้ดที่เลิกใช้ไปแล้วมาเขียนต่อ

ข้อที่แพงที่สุดคือข้อสาม การแยกไฟล์อย่างระมัดระวัง ทั้งเขียน parity test เทียบลำดับ hook และให้คนรีวิว ใช้เวลาเป็นวัน ถ้าทำกับโค้ดที่ตายแล้ว ทั้งหมดนั้นเสียเปล่า

อาการของโค้ดที่น่าจะตายแล้ว

  • props ที่มีไว้ให้ feature ที่ถอดออกไปแล้ว เช่น collaboration, legacyMode หรือ onChangeJson ที่ไม่มีหน้าไหนส่งมา
  • component ที่คนเรียกมีแค่ test ของตัวเองกับ story ใน Storybook
  • ไฟล์ที่ชื่อลงท้ายด้วย -old, -v1 หรือ -legacy ข้างไฟล์ชื่อเดียวกัน
  • export ที่ไม่มีใคร import นอกจากไฟล์ index.ts ที่ re-export ต่ออีกที
  • กิ่ง if ที่ตรวจ flag ซึ่งเปิดถาวรไปนานแล้ว

เช็กด้วย git grep ก่อน

วิธีที่เร็วที่สุดคือหาว่าใคร import ไฟล์นี้ โดยตัด test ออก

git grep -l "components/editor/slash-menu" -- 'apps/**' ':!**/__tests__/**' ':!**/*.test.*' ':!**/*.stories.*'

ถ้าไม่เจออะไรเลย ยังสรุปไม่ได้ทันที ต้องดูอีกสามทาง

  1. dynamic import เช่น import('./slash-menu') หรือ next/dynamic ซึ่งค้นด้วยชื่อไฟล์อย่างเดียวก็เจอ แต่ถ้าประกอบ path จากตัวแปร ต้องดูโค้ดที่ประกอบ
  2. ชื่อที่อ้างเป็น string เช่น registry ที่เก็บ 'slash-menu': SlashMenu หรือ config ที่ระบุชื่อ component เป็นข้อความ
  3. barrel file ถ้ามี index.ts ที่ re-export ต้องค้นชื่อ export (SlashMenu) ด้วย ไม่ใช่แค่ path

ถ้าครบสามทางแล้วยังไม่เจอ ก็มั่นใจได้ระดับหนึ่ง ขั้นถัดไปคือให้เครื่องมือช่วยดูทั้งโปรเจกต์

ใช้ knip --production ไล่ทั้งโปรเจกต์

Knip เป็นเครื่องมือฟรีที่ไล่ import graph จาก entry ของโปรเจกต์ แล้วรายงานไฟล์ export และ dependency ที่ไม่มีใครใช้ มี plugin สำหรับ Next.js ที่รู้จักไฟล์ page.tsx, layout.tsx และ route.ts เป็น entry ให้เอง

pnpm dlx knip --production

ตัวเลือก --production คือหัวใจของบทนี้ ปกติ knip นับ test และไฟล์ config เป็น entry ด้วย โค้ดที่มีแค่ test เรียกก็ยังดูเหมือนมีคนใช้ พอใส่ --production knip จะดูเฉพาะเส้นทางที่ไปถึงผู้ใช้จริง ไฟล์ที่ test เรียกอย่างเดียวจะขึ้นในรายการไฟล์ที่ไม่ได้ใช้

ใน monorepo ให้ตั้ง knip.json บอก workspace และ entry ที่ plugin หาเองไม่เจอ เช่น registry ที่โหลด plugin จากชื่อ

{
  "workspaces": {
    "apps/admin": {
      "entry": ["src/plugins/*/index.ts"],
      "project": ["src/**/*.{ts,tsx}"]
    }
  }
}

รายงานของ knip เป็นจุดเริ่ม ไม่ใช่คำตัดสิน ให้เปิดดูทีละรายการด้วยสามข้อในหัวข้อก่อนหน้า โดยเฉพาะของที่โหลดจาก string

ลบ props ที่ตายแล้วจะเผยให้เห็น module ที่ตายตามไปด้วย

โค้ดที่ตายแล้วมักมาเป็นชั้น ๆ component ที่ยังใช้อยู่อาจมี props บางตัวที่ไม่มีใครส่งมาแล้ว และ props พวกนั้นคือทางเดียวที่พาไปถึง module อีกหลายไฟล์

// ก่อน: props ที่ไม่มีใครส่ง แต่ละตัวพาไปถึงโค้ดอีกชุด
interface EditorProps {
  value: string;
  onChange(value: string): void;
  placeholder?: string;
  valueJson?: Doc;                    // ไม่มีหน้าไหนส่ง
  onChangeJson?(doc: Doc): void;      // ไม่มีหน้าไหนส่ง
  collaboration?: CollabConfig;       // feature ที่ถอดไปแล้ว
  slashMenu?: boolean;                // ไม่มีหน้าไหนเปิด
  dragHandles?: boolean;              // ไม่มีหน้าไหนเปิด
}

export function Editor(props: EditorProps) {
  const extensions = [
    ...baseExtensions,
    ...(props.collaboration ? collabExtensions(props.collaboration) : []),
    ...(props.slashMenu ? [SlashMenu] : []),
    ...(props.dragHandles ? [DragHandles] : []),
  ];
  // …
}

// หลัง: เหลือเฉพาะที่หน้าจริงใช้ และ import ของ collab, slash menu, drag handles หายไป
interface EditorProps {
  value: string;
  onChange(value: string): void;
  placeholder?: string;
}

export function Editor({ value, onChange, placeholder }: EditorProps) {
  const extensions = baseExtensions;
  // …
}

พอลบ props สามสี่ตัวนี้ import ของ collabExtensions, SlashMenu และ DragHandles ก็หายจากไฟล์ ไฟล์เหล่านั้นจึงเหลือแค่ test ของตัวเองเป็นคนเรียก และแต่ละไฟล์ก็อาจพาไปถึงไฟล์อื่นอีกต่อหนึ่ง

ลบ props แล้วต้องรันการเช็กซ้ำทุกครั้ง เพราะรอบแรกยังไม่เห็นไฟล์ชั้นถัดไป git grep รอบแรกเจอว่า SlashMenu มีคน import (คือ Editor) พอลบ prop แล้วค่อยเห็นว่าไม่มีใครเหลือ

ลบให้ปลอดภัย

  • ลบในงานของตัวเอง อย่ารวมกับการแยกไฟล์หรือแก้ bug ใน PR เดียวกัน คนรีวิวจะได้ดูแค่ว่าของที่ลบไม่มีใครใช้จริง
  • ลบ test ไปพร้อมกัน test ของโค้ดที่ตายแล้วไม่ได้ปกป้องอะไร แค่ทำให้ CI ช้า
  • ค้นหา spread ด้วย ถ้ามีที่ไหนเขียน <Editor {...props} /> props ที่คิดว่าไม่มีใครส่งอาจเดินทางมาทางนี้ ค้นหา {... รอบ ๆ ทุกที่ที่ใช้ component นั้น
  • อัปเดตเอกสารและ registry ถ้า component เป็นส่วนของ UI kit ที่มีหน้าตัวอย่างหรือเอกสาร ต้องแก้ในงานเดียวกัน ไม่อย่างนั้นหน้าตัวอย่างจะพัง
  • ให้ typecheck เป็นด่านแรก หลังลบ props ถ้ามีหน้าไหนยังส่งค่ามาจริง TypeScript จะฟ้องทันที

กันโค้ดตายใหม่ด้วย knip ใน CI

หาโค้ดที่ตายแล้วครั้งเดียวแล้วจบ ไม่นานก็กลับมาอีก เพราะทุกครั้งที่ถอด feature มักลบแค่ส่วนที่เห็น เช่น ปุ่มหรือหน้า แล้วปล่อย component กับ helper ข้างหลังไว้ ทางที่ได้ผลคือให้ CI เตือนตั้งแต่วันที่เกิด

{
  "scripts": {
    "knip": "knip --production --include files,exports"
  }
}

เริ่มจากรายงานเฉพาะไฟล์และ export ที่ไม่ได้ใช้ก่อน ซึ่งเป็นสองอย่างที่ชัดที่สุด ส่วน dependency ที่ไม่ได้ใช้และ type ที่ไม่ได้ใช้ค่อยเปิดทีหลังเมื่อรายการแรกสะอาดแล้ว

โปรเจกต์เดิมที่รันครั้งแรกอาจเจอเป็นร้อยรายการ ไม่ต้องเคลียร์ทั้งหมดในวันเดียว ให้ใส่สิ่งที่ยังไม่พร้อมลบไว้ใน ignore ของ knip.json พร้อมเหตุผล แล้วให้ CI ล้มเฉพาะรายการใหม่ รายการ ignore จะค่อย ๆ สั้นลงทุกครั้งที่มีคนเข้าไปแก้ไฟล์แถวนั้น

ข้อดีอีกอย่างคือ knip ทำงานกับ import graph จริง ตอนแยกไฟล์ใหญ่ออกเป็นหลายไฟล์ ถ้าลืม export บางตัวไว้ในไฟล์ใหม่โดยไม่มีใครใช้ knip ก็จะชี้ให้เห็นทันที

เมื่อไหร่ไม่ควรรีบลบ

  • API สาธารณะของ package ที่โปรเจกต์อื่นอาจใช้อยู่ ให้ทำเครื่องหมาย deprecated แล้วลบใน major version ถัดไป
  • feature ที่ปิดชั่วคราวด้วย flag ถ้าทีมยังจะเปิดกลับมา ให้ถามก่อน แล้วจดไว้ในโค้ดหรือเอกสารว่าทำไมยังเก็บ
  • ของที่โหลดจากข้อมูล เช่น block ที่ผู้ใช้เลือกใส่ในหน้าเว็บจาก CMS แล้วเก็บชื่อไว้ในฐานข้อมูล โค้ดไม่ได้ import ตรง ๆ แต่ยังมีหน้าที่ใช้อยู่ ต้องค้นในข้อมูลจริงด้วย

จากงานจริง

ในร้านค้าออนไลน์ Next.js 16 กับ CMS ที่เป็น Next.js admin ใน pnpm monorepo ที่เรา refactor ไป รอบหลัง ๆ เราลบ props แปดตัวของ rich text editor ที่มีไว้ให้ feature ที่ถอดไปแล้ว พอลบเสร็จ ปรากฏว่ามี module อีกราวสิบห้าไฟล์ รวมประมาณ 3,000 บรรทัด ที่เหลือแค่ test ของตัวเองเป็นคนเรียก ไฟล์หนึ่งในนั้นเราเพิ่งแยกไฟล์อย่างละเอียดไปเมื่อรอบก่อน พร้อม parity test และการรีวิว ซึ่งเสียเปล่าทั้งหมด ตั้งแต่นั้นการไล่ import graph จาก entry ของแอปโดยไม่นับ test กลายเป็นขั้นแรกของทุกงาน refactor ก่อนจะเลือกว่าไฟล์ไหนควรแยก

จบบทเรียน สอนเขียน React และ Next.js ให้ดูแลต่อได้ แล้ว

ดูทั้ง 14 บท →

อ่านต่อ

ล่าสุด

ดูทั้งหมด →