Clean Code & Performance
ປຽບທຽບໂຄດແບບທຳມະດາ ກັບໂຄດທີ່ຖືກປັບປຸງໃຫ້ສະອາດ ແລະ ໄວຂຶ້ນ — ຝັ່ງຊ້າຍຄືກ່ອນປັບ, ຝັ່ງຂວາຄືຫຼັງປັບ
Frontend — React / Next.js
ຫຼີກເວັ້ນ State ຊ້ຳຊ້ອນ — ໃຊ້ useMemo ແທນ useEffect
ຄ່າທີ່ຄິດໄລ່ໄດ້ຈາກ props/state ຢູ່ແລ້ວ ບໍ່ຄວນເກັບໃສ່ state ອີກອັນຜ່ານ useEffect ເພາະຈະ render ຊ້ຳສອງເທື່ອທຸກຄັ້ງທີ່ພິມ. ຄິດໄລ່ມັນລະຫວ່າງ render ດ້ວຍ useMemo ດີກວ່າ.
1function ProductList({ products }) {2 const [query, setQuery] = useState('')3 const [filtered, setFiltered] = useState([])45 // redundant state + useEffect6 // -> renders twice on every keystroke7 useEffect(() => {8 setFiltered(9 products.filter((p) =>10 p.name.toLowerCase().includes(query.toLowerCase())11 )12 )13 }, [products, query])1415 return (16 <div>17 <input value={query} onChange={(e) => setQuery(e.target.value)} />18 {filtered.map((p) => (19 <ProductItem key={p.id} product={p} />20 ))}21 </div>22 )23}
1function ProductList({ products }) {2 const [query, setQuery] = useState('')34 // derived state: computed during render,5 // no extra state, no extra render6 const filtered = useMemo(() => {7 const q = query.toLowerCase()8 return products.filter((p) => p.name.toLowerCase().includes(q))9 }, [products, query])1011 return (12 <div>13 <input value={query} onChange={(e) => setQuery(e.target.value)} />14 {filtered.map((p) => (15 <ProductItem key={p.id} product={p} />16 ))}17 </div>18 )19}
Early Return ແທນ Ternary ຊ້ອນກັນ
Ternary ຊ້ອນກັນຫຼາຍຊັ້ນອ່ານຍາກ ແລະ ແກ້ບັກຍາກ. ໃຊ້ guard clause return ອອກກ່ອນ ເຮັດໃຫ້ເສັ້ນທາງຫຼັກຂອງ component ຈະແຈ້ງຂຶ້ນຫຼາຍ.
1function UserCard({ user, isLoading, error }) {2 return (3 <div>4 {isLoading ? (5 <Spinner />6 ) : error ? (7 <p>{error.message}</p>8 ) : user ? (9 <div>10 <h2>{user.name}</h2>11 {user.isAdmin ? (12 <AdminBadge />13 ) : user.isPro ? (14 <ProBadge />15 ) : null}16 </div>17 ) : (18 <p>No user found</p>19 )}20 </div>21 )22}
1function UserCard({ user, isLoading, error }) {2 // guard clauses: fail fast, no nesting3 if (isLoading) return <Spinner />4 if (error) return <p>{error.message}</p>5 if (!user) return <p>No user found</p>67 return (8 <div>9 <h2>{user.name}</h2>10 <UserBadge role={user.role} />11 </div>12 )13}
ຢຸດ Re-render ທີ່ບໍ່ຈຳເປັນໃນ List
ໃຊ້ index ເປັນ key ແລະ ສ້າງ function/object ໃໝ່ທຸກ render ເຮັດໃຫ້ທຸກ item render ຄືນໝົດເວລາກົດອັນດຽວ. ໃຊ້ key ຄົງທີ່ + memo + useCallback ໃຫ້ render ສະເພາະ item ທີ່ປ່ຽນແທ້ໆ.
1function TodoList({ todos }) {2 const [selectedId, setSelectedId] = useState(null)34 return (5 <ul>6 {todos.map((todo, index) => (7 // index as key + new function/object8 // created on every render9 // -> every item re-renders on each click10 <TodoItem11 key={index}12 todo={todo}13 style={{ padding: 8, borderRadius: 6 }}14 onSelect={() => setSelectedId(todo.id)}15 />16 ))}17 </ul>18 )19}
1const TodoItem = memo(function TodoItem({ todo, onSelect }) {2 return <li onClick={() => onSelect(todo.id)}>{todo.title}</li>3})45const itemStyle = { padding: 8, borderRadius: 6 }67function TodoList({ todos }) {8 const [selectedId, setSelectedId] = useState(null)910 // stable reference -> memo() can skip11 // items whose props did not change12 const handleSelect = useCallback((id) => setSelectedId(id), [])1314 return (15 <ul>16 {todos.map((todo) => (17 <TodoItem key={todo.id} todo={todo} onSelect={handleSelect} />18 ))}19 </ul>20 )21}
ລວມ Logic ການ Fetch ໄວ້ໃນ Custom Hook
ໂຄດ fetch ທີ່ copy ຊ້ຳກັນທຸກ component ແລະ ບໍ່ຍົກເລີກ request ເກົ່າ ອາດເຮັດໃຫ້ຂໍ້ມູນເກົ່າມາທັບຂໍ້ມູນໃໝ່ (race condition). Custom hook + AbortController ແກ້ທັງສອງບັນຫາໃນບ່ອນດຽວ.
1function BlogPage() {2 const [posts, setPosts] = useState([])3 const [loading, setLoading] = useState(true)45 useEffect(() => {6 // same logic copy-pasted in every page,7 // no cleanup: a slow old request can8 // overwrite a newer response9 fetch('/api/posts')10 .then((res) => res.json())11 .then((data) => {12 setPosts(data)13 setLoading(false)14 })15 }, [])1617 if (loading) return <Spinner />18 return <PostGrid posts={posts} />19}
1function useFetch(url) {2 const [state, setState] = useState({3 data: null, loading: true, error: null,4 })56 useEffect(() => {7 const controller = new AbortController()8 fetch(url, { signal: controller.signal })9 .then((res) => res.json())10 .then((data) => setState({ data, loading: false, error: null }))11 .catch((error) => {12 if (error.name !== 'AbortError')13 setState({ data: null, loading: false, error })14 })15 // cancel the stale request on unmount / url change16 return () => controller.abort()17 }, [url])1819 return state20}2122function BlogPage() {23 const { data: posts, loading } = useFetch('/api/posts')2425 if (loading) return <Spinner />26 return <PostGrid posts={posts} />27}
ຫຼຸດຂະໜາດ Bundle ດ້ວຍ Dynamic Import
Import library ໜັກໆແບບ static ເຮັດໃຫ້ໜ້າທຳອິດໂຫຼດຊ້າ ເຖິງແມ່ນຜູ້ໃຊ້ບໍ່ເຄີຍໃຊ້ feature ນັ້ນເລີຍ. next/dynamic ຈະແຍກ bundle ແລະ ໂຫຼດສະເພາະຕອນຕ້ອງໃຊ້ແທ້ໆ.
1import HeavyChart from 'heavy-chart-lib' // 300kB+2import ExportPDF from './ExportPDF' // rarely used34function Dashboard({ stats }) {5 const [showExport, setShowExport] = useState(false)67 // everything ships in the first bundle,8 // slow first paint even if the user9 // never opens the export dialog10 return (11 <div>12 <HeavyChart data={stats} />13 <button onClick={() => setShowExport(true)}>Export</button>14 {showExport && <ExportPDF data={stats} />}15 </div>16 )17}
1import dynamic from 'next/dynamic'23// loaded only when rendered -> smaller first bundle4const HeavyChart = dynamic(() => import('heavy-chart-lib'), {5 loading: () => <ChartSkeleton />,6 ssr: false,7})8const ExportPDF = dynamic(() => import('./ExportPDF'))910function Dashboard({ stats }) {11 const [showExport, setShowExport] = useState(false)1213 return (14 <div>15 <HeavyChart data={stats} />16 <button onClick={() => setShowExport(true)}>Export</button>17 {showExport && <ExportPDF data={stats} />}18 </div>19 )20}
Backend — Node.js / Express / MongoDB
ແກ້ບັນຫາ N+1 Query
Query ຖານຂໍ້ມູນໃນ loop ເຮັດໃຫ້ 100 ລາຍການ = 101 ຄັ້ງຕໍ່ຖານຂໍ້ມູນ. ດຶງມາເທື່ອດຽວດ້ວຍ $in ແລ້ວ join ໃນ memory — ເຫຼືອພຽງ 2 queries ບໍ່ວ່າຈະມີຈັກລາຍການ.
1app.get('/api/orders', async (req, res) => {2 const orders = await Order.find()34 // N+1 problem: 1 query + 1 more per order5 // 100 orders = 101 database round trips6 for (const order of orders) {7 order.customer = await User.findById(order.userId)8 }910 res.json(orders)11})
1app.get('/api/orders', async (req, res) => {2 const orders = await Order.find().lean()34 // 2 queries total, no matter how many orders5 const userIds = orders.map((o) => o.userId)6 const users = await User.find({ _id: { $in: userIds } }).lean()7 const userById = new Map(users.map((u) => [String(u._id), u]))89 res.json(10 orders.map((o) => ({11 ...o,12 customer: userById.get(String(o.userId)),13 }))14 )15})
ໃຊ້ Promise.all ກັບວຽກທີ່ບໍ່ຂຶ້ນຕໍ່ກັນ
await ຕໍ່ແຖວກັນເຮັດໃຫ້ເວລາລວມ = ຜົນບວກຂອງທຸກ query. ຖ້າ query ບໍ່ຂຶ້ນຕໍ່ກັນ ໃຫ້ run ພ້ອມກັນ — ເວລາລວມຈະເທົ່າກັບ query ທີ່ຊ້າສຸດອັນດຽວ.
1app.get('/api/dashboard', async (req, res) => {2 // each await waits for the previous one3 // total time = query1 + query2 + query34 const user = await User.findById(req.userId)5 const orders = await Order.find({ userId: req.userId })6 const alerts = await Notification.find({ userId: req.userId })78 res.json({ user, orders, alerts })9})
1app.get('/api/dashboard', async (req, res) => {2 // independent queries run in parallel3 // total time = the slowest query only4 const [user, orders, alerts] = await Promise.all([5 User.findById(req.userId),6 Order.find({ userId: req.userId }),7 Notification.find({ userId: req.userId }),8 ])910 res.json({ user, orders, alerts })11})
Pagination + ເລືອກສະເພາະ Field ທີ່ໃຊ້
ດຶງທຸກ record ທຸກ field ຈະຊ້າລົງເລື້ອຍໆເມື່ອຂໍ້ມູນເພີ່ມຂຶ້ນ. ໃຊ້ limit/skip + select ດຶງສະເພາະໜ້າດຽວ ແລະ ສະເພາະ field ທີ່ໜ້າ list ຕ້ອງໃຊ້ແທ້.
1app.get('/api/products', async (req, res) => {2 // loads EVERY product with EVERY field3 // into memory on each request4 // -> slow response + heavy payload5 // that keeps growing with the data6 const products = await Product.find()78 res.json(products)9})
1app.get('/api/products', async (req, res) => {2 const page = Math.max(1, Number(req.query.page) || 1)3 const limit = Math.min(50, Number(req.query.limit) || 20)45 // one page only, with just the fields the list needs6 const [items, total] = await Promise.all([7 Product.find()8 .select('name price thumbnail')9 .skip((page - 1) * limit)10 .limit(limit)11 .lean(),12 Product.countDocuments(),13 ])1415 res.json({ items, total, page, pages: Math.ceil(total / limit) })16})
Guard Clause + ຈັດການ Error ໃຫ້ປອດໄພ
if ຊ້ອນກັນເລິກ, save req.body ໂດຍກົງ ແລະ ສົ່ງ error ພາຍໃນອອກໄປຫາຜູ້ໃຊ້ ເປັນທັງບັນຫາຄວາມສະອາດ ແລະ ຄວາມປອດໄພ. Validate ແບບ fail-fast, whitelist field ແລະ ສົ່ງ error ໃຫ້ handler ກາງຈັດການ.
1app.post('/api/users', async (req, res) => {2 if (req.body.email) {3 if (req.body.password) {4 if (req.body.password.length >= 8) {5 try {6 // saves ANY field the client sends7 const user = await User.create(req.body)8 res.json(user) // leaks password hash9 } catch (e) {10 // leaks internal details to the client11 res.status(500).json({ error: e.message })12 }13 } else {14 res.status(400).json({ error: 'Password too short' })15 }16 } else {17 res.status(400).json({ error: 'Password required' })18 }19 } else {20 res.status(400).json({ error: 'Email required' })21 }22})
1app.post('/api/users', async (req, res, next) => {2 try {3 const { email, password } = req.body45 // guard clauses: fail fast, no nesting6 if (!email) return res.status(400).json({ error: 'Email required' })7 if (!password || password.length < 8)8 return res.status(400).json({ error: 'Password must be 8+ chars' })910 // whitelist fields - never save req.body directly11 const user = await User.create({ email, password })12 const { password: _, ...safeUser } = user.toObject()1314 res.status(201).json(safeUser)15 } catch (err) {16 next(err) // central error handler logs & hides internals17 }18})
Cache ຜົນລັບທີ່ຄິດໄລ່ໜັກດ້ວຍ Redis
Aggregation ໜັກໆທີ່ຜົນລັບເກືອບບໍ່ປ່ຽນ ບໍ່ຄວນ run ໃໝ່ທຸກ request. Cache ໄວ້ 5 ນາທີ ຫຼຸດເວລາຕອບຈາກຫຼາຍຮ້ອຍ ms ເຫຼືອປະມານ 1ms ແລະ ຫຼຸດພາລະຖານຂໍ້ມູນລົງມະຫາສານ.
1app.get('/api/stats', async (req, res) => {2 // heavy aggregation runs on EVERY request3 // even though the result barely changes4 const stats = await Order.aggregate([5 { $group: { _id: '$productId', total: { $sum: '$amount' } } },6 { $sort: { total: -1 } },7 { $limit: 10 },8 ])910 res.json(stats)11})
1const CACHE_KEY = 'stats:top-products'2const TTL_SECONDS = 300 // fresh enough for a dashboard34app.get('/api/stats', async (req, res) => {5 const cached = await redis.get(CACHE_KEY)6 if (cached) return res.json(JSON.parse(cached)) // ~1ms78 const stats = await Order.aggregate([9 { $group: { _id: '$productId', total: { $sum: '$amount' } } },10 { $sort: { total: -1 } },11 { $limit: 10 },12 ])1314 await redis.set(CACHE_KEY, JSON.stringify(stats), 'EX', TTL_SECONDS)15 res.json(stats)16})