Skip to content

Commit 20b5b77

Browse files
CopilotKeyyarddeepsource-autofix[bot]
authored
Performance: Eliminate redundant animation loops and unnecessary re-renders (#42)
* Initial plan * Performance optimizations: Fix redundant loops, add React.memo, and optimize re-renders Co-authored-by: Keyyard <84187238+Keyyard@users.noreply.github.com> * style: format code with Prettier This commit fixes the style issues introduced in da9bfba according to the output from Prettier. Details: #42 * Add debouncing to resize handler and optimize Contact component Co-authored-by: Keyyard <84187238+Keyyard@users.noreply.github.com> * style: format code with Prettier This commit fixes the style issues introduced in e5196ac according to the output from Prettier. Details: #42 * Optimize discovery pages with React.memo and useMemo for filtering Co-authored-by: Keyyard <84187238+Keyyard@users.noreply.github.com> * style: format code with Prettier This commit fixes the style issues introduced in 8da2814 according to the output from Prettier. Details: #42 * Address code review feedback: Use useRef for timeout and add null check Co-authored-by: Keyyard <84187238+Keyyard@users.noreply.github.com> * Add React.memo to all section components for better performance Co-authored-by: Keyyard <84187238+Keyyard@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Keyyard <84187238+Keyyard@users.noreply.github.com> Co-authored-by: deepsource-autofix[bot] <62050782+deepsource-autofix[bot]@users.noreply.github.com>
1 parent 8e282f4 commit 20b5b77

File tree

13 files changed

+522
-401
lines changed

13 files changed

+522
-401
lines changed

eslint.config.js

Lines changed: 23 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,46 @@
1-
import js from '@eslint/js'
2-
import globals from 'globals'
3-
import react from 'eslint-plugin-react'
4-
import reactHooks from 'eslint-plugin-react-hooks'
5-
import reactRefresh from 'eslint-plugin-react-refresh'
6-
import typescript from '@typescript-eslint/eslint-plugin'
7-
import parser from '@typescript-eslint/parser'
8-
import next from 'eslint-plugin-next'
1+
import js from "@eslint/js";
2+
import globals from "globals";
3+
import react from "eslint-plugin-react";
4+
import reactHooks from "eslint-plugin-react-hooks";
5+
import reactRefresh from "eslint-plugin-react-refresh";
6+
import typescript from "@typescript-eslint/eslint-plugin";
7+
import parser from "@typescript-eslint/parser";
98

109
export default [
11-
{ ignores: ['dist', '.next'] },
10+
{ ignores: ["dist", ".next", "out", "node_modules"] },
1211
{
13-
files: ['**/*.{js,jsx,ts,tsx}'],
12+
files: ["**/*.{js,jsx,ts,tsx}"],
1413
languageOptions: {
1514
ecmaVersion: 2020,
1615
globals: globals.browser,
1716
parser: parser,
1817
parserOptions: {
19-
ecmaVersion: 'latest',
18+
ecmaVersion: "latest",
2019
ecmaFeatures: { jsx: true },
21-
sourceType: 'module',
20+
sourceType: "module",
2221
},
2322
},
24-
settings: { react: { version: '18.3' } },
23+
settings: { react: { version: "18.3" } },
2524
plugins: {
2625
react,
27-
'react-hooks': reactHooks,
28-
'react-refresh': reactRefresh,
29-
'@typescript-eslint': typescript,
30-
next,
26+
"react-hooks": reactHooks,
27+
"react-refresh": reactRefresh,
28+
"@typescript-eslint": typescript,
3129
},
3230
rules: {
3331
...js.configs.recommended.rules,
3432
...react.configs.recommended.rules,
35-
...react.configs['jsx-runtime'].rules,
33+
...react.configs["jsx-runtime"].rules,
3634
...reactHooks.configs.recommended.rules,
3735
...typescript.configs.recommended.rules,
38-
...next.configs.recommended.rules,
39-
'react/jsx-no-target-blank': 'off',
40-
'react-refresh/only-export-components': [
41-
'warn',
36+
"react/jsx-no-target-blank": "off",
37+
"react/prop-types": "off", // Using TypeScript for prop validation
38+
"react-refresh/only-export-components": [
39+
"warn",
4240
{ allowConstantExport: true },
4341
],
44-
'@typescript-eslint/no-unused-vars': 'off',
45-
'react/no-unknown-property': 'off',
42+
"@typescript-eslint/no-unused-vars": "off",
43+
"react/no-unknown-property": "off",
4644
},
4745
},
48-
]
46+
];
Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,31 @@
11
import Link from "next/link";
2-
import React from "react";
2+
import React, { memo } from "react";
33
import { PostMeta } from "../../utils/markdown";
44
import SafeImage from "../ui/SafeImage";
55

66
type Props = {
77
post: PostMeta;
88
};
99

10-
export default function DiscoveryCard({ post }: Props) {
10+
const DiscoveryCard = memo(({ post }: Props) => {
1111
return (
1212
<article className="group h-full">
13-
<Link href={`/discovery/${post.slug}`} className="project-card discovery-card block h-full flex flex-col">
13+
<Link
14+
href={`/discovery/${post.slug}`}
15+
className="project-card discovery-card block h-full flex flex-col"
16+
>
1417
{/* Fixed-height image area so cards align evenly */}
1518
<div className="w-full h-56 overflow-hidden rounded-t">
1619
{post.image ? (
17-
<SafeImage src={post.image} alt={post.title} className="w-full h-full object-cover" />
20+
<SafeImage
21+
src={post.image}
22+
alt={post.title}
23+
className="w-full h-full object-cover"
24+
/>
1825
) : (
19-
<div className="w-full h-full bg-gray-800 flex items-center justify-center text-gray-400">No Image</div>
26+
<div className="w-full h-full bg-gray-800 flex items-center justify-center text-gray-400">
27+
No Image
28+
</div>
2029
)}
2130
</div>
2231

@@ -27,7 +36,10 @@ export default function DiscoveryCard({ post }: Props) {
2736
<p className="project-subtitle">{post.description}</p>
2837
<div className="mt-3 flex flex-wrap gap-2">
2938
{post.tags?.map((t) => (
30-
<span key={t} className="text-xs bg-gray-700 text-gray-200 px-2 py-1 rounded">
39+
<span
40+
key={t}
41+
className="text-xs bg-gray-700 text-gray-200 px-2 py-1 rounded"
42+
>
3143
{t}
3244
</span>
3345
))}
@@ -36,4 +48,8 @@ export default function DiscoveryCard({ post }: Props) {
3648
</Link>
3749
</article>
3850
);
39-
}
51+
});
52+
53+
DiscoveryCard.displayName = "DiscoveryCard";
54+
55+
export default DiscoveryCard;

src/components/discovery/DiscoveryList.tsx

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React from "react";
1+
import React, { useMemo } from "react";
22
import { PostMeta } from "../../utils/markdown";
33
import DiscoveryCard from "./DiscoveryCard";
44

@@ -8,13 +8,20 @@ type Props = {
88

99
export default function DiscoveryList({ posts }: Props) {
1010
if (!posts.length)
11-
return <p className="text-center text-gray-400">No posts yet, come back later?</p>;
12-
// Ensure posts are sorted by date (newest first). Some posts may omit dates.
13-
const sorted = [...posts].sort((a, b) => {
14-
const da = a.date ? new Date(a.date).getTime() : 0;
15-
const db = b.date ? new Date(b.date).getTime() : 0;
16-
return db - da; //db - da for descending order (newest first)
17-
});
11+
return (
12+
<p className="text-center text-gray-400">
13+
No posts yet, come back later?
14+
</p>
15+
);
16+
17+
// Ensure posts are sorted by date (newest first). Memoize to avoid re-sorting on every render.
18+
const sorted = useMemo(() => {
19+
return [...posts].sort((a, b) => {
20+
const da = a.date ? new Date(a.date).getTime() : 0;
21+
const db = b.date ? new Date(b.date).getTime() : 0;
22+
return db - da; //db - da for descending order (newest first)
23+
});
24+
}, [posts]);
1825

1926
return (
2027
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-4 p-6 pb-32">
Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
const Bg = () => {
2-
return (
3-
<div className="absolute top-0 left-0 z-[-1] w-full h-full">
4-
</div>
5-
);
6-
};
1+
import { memo } from "react";
2+
3+
const Bg = memo(() => {
4+
return <div className="absolute top-0 left-0 z-[-1] w-full h-full"></div>;
5+
});
6+
7+
Bg.displayName = "Background";
78

89
export default Bg;

src/components/layout/MyHead.tsx

Lines changed: 22 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { useGLTF } from "@react-three/drei";
22
import { Canvas, useFrame } from "@react-three/fiber";
33
import { easing } from "maath";
4-
import { useEffect, useRef, useState } from "react";
4+
import { useEffect, useRef, useState, memo } from "react";
55
import { Object3D, LoopOnce, AnimationMixer } from "three";
66
function Model({ onLoaded }: { onLoaded: () => void }) {
77
const gltf = useGLTF("/assets/myHead.gltf");
@@ -38,33 +38,31 @@ function Model({ onLoaded }: { onLoaded: () => void }) {
3838
};
3939
}, [scene, animations]);
4040

41-
useEffect(() => {
42-
const animate = () => {
43-
requestAnimationFrame(animate);
44-
if (mixer.current) {
45-
mixer.current.update(0.01);
46-
}
47-
};
48-
animate();
49-
}, []);
50-
5141
useEffect(() => {
5242
if (head.current) {
5343
head.current.rotation.set(0, Math.PI, 0);
5444
head.current.position.set(0, -0.5, 0);
5545
}
5646
}, []);
5747

58-
useFrame((state, dt) => {
59-
dummy.lookAt(cursor.x, cursor.y, 1);
60-
dummy.rotation.y += Math.PI;
61-
easing.dampQ(head.current.quaternion, dummy.quaternion, 0.15, dt);
48+
useFrame((state, delta) => {
49+
// Update animation mixer if active
50+
if (mixer.current) {
51+
mixer.current.update(delta);
52+
}
53+
54+
// Update head rotation to follow cursor
55+
if (head.current) {
56+
dummy.lookAt(cursor.x, cursor.y, 1);
57+
dummy.rotation.y += Math.PI;
58+
easing.dampQ(head.current.quaternion, dummy.quaternion, 0.15, delta);
59+
}
6260
});
6361

6462
return <primitive object={head.current} />;
6563
}
6664

67-
const HeadRender = () => {
65+
const HeadRender = memo(() => {
6866
const [loading, setLoading] = useState(true);
6967
return (
7068
<div className="relative w-full h-[600px]">
@@ -73,12 +71,18 @@ const HeadRender = () => {
7371
<div className="animate-pulse bg-gray-300 rounded-full w-40 h-40" />
7472
</div>
7573
)}
76-
<Canvas camera={{ position: [0, 0, 6], fov: 50 }} id="head" className="w-full h-full">
74+
<Canvas
75+
camera={{ position: [0, 0, 6], fov: 50 }}
76+
id="head"
77+
className="w-full h-full"
78+
>
7779
<ambientLight intensity={0.5} />
7880
<Model onLoaded={() => setLoading(false)} />
7981
</Canvas>
8082
</div>
8183
);
82-
};
84+
});
85+
86+
HeadRender.displayName = "HeadRender";
8387

8488
export default HeadRender;

src/components/layout/Nav.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,9 @@ const Nav = () => {
2727
(entries) => {
2828
entries.forEach((entry) => {
2929
if (!entry.isIntersecting) return;
30-
setActiveSection(entry.target.id);
30+
// Only update if the active section has actually changed
31+
const newSection = entry.target.id;
32+
setActiveSection((prev) => (prev !== newSection ? newSection : prev));
3133
});
3234
},
3335
{ threshold: 0.01 },

0 commit comments

Comments
 (0)