diff --git a/PERFORMANCE.md b/PERFORMANCE.md
new file mode 100644
index 0000000..37c95c9
--- /dev/null
+++ b/PERFORMANCE.md
@@ -0,0 +1,63 @@
+# Performance Report
+
+## Before Optimization
+
+### 1. Toggle columns
+- Commit duration: 19.8ms
+- Render duration: 961.9ms
+- Screenshot: 
+
+### 2. Searching
+- Commit duration: 15.8ms
+- Render duration: 486.3ms
+- Screenshot: 
+
+### 3. Select year
+- Commit duration: 5.7ms
+- Render duration: 559.3ms
+- Screenshot: 
+
+### 4. Sorting
+- Commit duration: 5.4ms
+- Render duration: 462.7ms
+- Screenshot: 
+
+## After optimization
+
+### 1. Toggle columns
+- Commit duration: 5.1ms
+- Render duration: 29.4ms
+- Screenshot: 
+
+### 2. Searching
+- Commit duration: 4.7ms
+- Render duration: 87ms
+- Screenshot: 
+
+### 3. Select year
+- Commit duration: 8ms
+- Render duration: 34.3ms
+- Screenshot: 
+
+### 4. Sorting
+- Commit duration: 4.6ms
+- Render duration: 94.8ms
+- Screenshot: 
+
+
+## Comparison
+
+| Action | Before | After | Improvement |
+|---|---|---|---|
+| Toggle columns | 961.9ms | 29.4ms | ~97% |
+| Searching | 486.3ms | 87ms | ~82% |
+| Select year | 559.3ms | 34.3ms | ~94% |
+| Sorting | 462.7ms | 94.8ms | ~80% |
+
+## Applied optimizations
+
+- `useMemo` — memoized filtered/sorted country list and computed values in CountryCard
+- `useCallback` — memoized event handlers in App
+- `React.memo` — prevented unnecessary re-renders of CountryCard
+- Proper `key` props — used `country.id` instead of index
+- Virtualization — rendered only visible rows with `react-window`
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
index 89aafd4..a8f8531 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -9,7 +9,8 @@
"version": "0.0.0",
"dependencies": {
"react": "^19.2.0",
- "react-dom": "^19.2.0"
+ "react-dom": "^19.2.0",
+ "react-window": "^2.2.7"
},
"devDependencies": {
"@eslint/js": "^9.39.1",
@@ -59,7 +60,6 @@
"integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@babel/code-frame": "^7.29.0",
"@babel/generator": "^7.29.0",
@@ -1526,7 +1526,6 @@
"integrity": "sha512-68e+T28EbdmLSTkPgs3+UacC6rzmqrcWFPQs1C8mwJhI/r5Uxr0yEuQotczNRROd1gq30NGxee+fo0rSIxpyAw==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"undici-types": "~7.16.0"
}
@@ -1537,7 +1536,6 @@
"integrity": "sha512-KkiJeU6VbYbUOp5ITMIc7kBfqlYkKA5KhEHVrGMmUUMt7NeaZg65ojdPk+FtNrBAOXNVM5QM72jnADjM+XVRAQ==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"csstype": "^3.2.2"
}
@@ -1607,7 +1605,6 @@
"integrity": "sha512-BtE0k6cjwjLZoZixN0t5AKP0kSzlGu7FctRXYuPAm//aaiZhmfq1JwdYpYr1brzEspYyFeF+8XF5j2VK6oalrA==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@typescript-eslint/scope-manager": "8.54.0",
"@typescript-eslint/types": "8.54.0",
@@ -1859,7 +1856,6 @@
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"dev": true,
"license": "MIT",
- "peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -1978,7 +1974,6 @@
}
],
"license": "MIT",
- "peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.9.0",
"caniuse-lite": "^1.0.30001759",
@@ -2200,7 +2195,6 @@
"integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.1",
@@ -3019,7 +3013,6 @@
"integrity": "sha512-tCmoRkPQLpBeWzpmbhryairGnhW9tKV6c6gr/w+RhoRoKEJwsjzipwp//1oCpGPOchvSLaAPlpcJi9MwMmoPyA==",
"dev": true,
"license": "MIT",
- "peer": true,
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/preact"
@@ -3066,7 +3059,6 @@
"resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz",
"integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==",
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -3076,7 +3068,6 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz",
"integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==",
"license": "MIT",
- "peer": true,
"dependencies": {
"scheduler": "^0.27.0"
},
@@ -3649,6 +3640,16 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/react-window": {
+ "version": "2.2.7",
+ "resolved": "https://registry.npmjs.org/react-window/-/react-window-2.2.7.tgz",
+ "integrity": "sha512-SH5nvfUQwGHYyriDUAOt7wfPsfG9Qxd6OdzQxl5oQ4dsSsUicqQvjV7dR+NqZ4coY0fUn3w1jnC5PwzIUWEg5w==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": "^18.0.0 || ^19.0.0",
+ "react-dom": "^18.0.0 || ^19.0.0"
+ }
+ },
"node_modules/resolve-from": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
@@ -3675,7 +3676,6 @@
"integrity": "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@types/estree": "1.0.8"
},
@@ -3866,7 +3866,6 @@
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"license": "Apache-2.0",
- "peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -3968,7 +3967,6 @@
"integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"esbuild": "^0.27.0",
"fdir": "^6.5.0",
@@ -4098,7 +4096,6 @@
"integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
"dev": true,
"license": "MIT",
- "peer": true,
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
diff --git a/package.json b/package.json
index 204350f..757f147 100644
--- a/package.json
+++ b/package.json
@@ -13,7 +13,8 @@
},
"dependencies": {
"react": "^19.2.0",
- "react-dom": "^19.2.0"
+ "react-dom": "^19.2.0",
+ "react-window": "^2.2.7"
},
"devDependencies": {
"@eslint/js": "^9.39.1",
diff --git a/src/components/app/app.tsx b/src/components/app/app.tsx
index 6ae81df..24ff6b8 100644
--- a/src/components/app/app.tsx
+++ b/src/components/app/app.tsx
@@ -1,4 +1,4 @@
-import { useState } from 'react';
+import { useCallback, useState } from 'react';
import { useCo2Data } from '../../hooks/useCo2Data';
import { LoadingSpinner } from '../loading-spinner/loading-spinner';
import { SearchBar } from '../search-bar/search-bar';
@@ -35,37 +35,37 @@ export const App = () => {
const years = data ? getAvailableYears(data) : [];
const availableColumns = getAvailableColumns();
- const handleSearch = (value: string) => {
- setState({ ...state, searchQuery: value });
- };
-
- const handleYearChange = (year: number) => {
- setState({ ...state, selectedYear: year });
- };
-
- const handleSortFieldChange = (field: 'name' | 'population') => {
- setState({ ...state, sortField: field });
- };
-
- const handleSortOrderToggle = () => {
- setState({
- ...state,
- sortOrder: state.sortOrder === 'asc' ? 'desc' : 'asc',
- });
- };
-
- const handleColumnToggle = (column: string) => {
- setState({
- ...state,
- selectedColumns: state.selectedColumns.includes(column)
- ? state.selectedColumns.filter((c) => c !== column)
- : [...state.selectedColumns, column],
- });
- };
-
- const handleModalToggle = () => {
- setState({ ...state, isColumnModalOpen: !state.isColumnModalOpen });
- };
+ const handleSearch = useCallback((value: string) => {
+ setState(prev => ({ ...prev, searchQuery: value }));
+ }, []);
+
+ const handleYearChange = useCallback((year: number) => {
+ setState(prev => ({ ...prev, selectedYear: year }));
+ }, []);
+
+ const handleSortFieldChange = useCallback((field: 'name' | 'population') => {
+ setState(prev =>({ ...prev, sortField: field }));
+ }, []);
+
+ const handleSortOrderToggle = useCallback(() => {
+ setState(prev => ({
+ ...prev,
+ sortOrder: prev.sortOrder === 'asc' ? 'desc' : 'asc',
+ }));
+ }, []);
+
+ const handleColumnToggle = useCallback((column: string) => {
+ setState(prev => ({
+ ...prev,
+ selectedColumns: prev.selectedColumns.includes(column)
+ ? prev.selectedColumns.filter((c) => c !== column)
+ : [...prev.selectedColumns, column],
+ }));
+ }, []);
+
+ const handleModalToggle = useCallback(() => {
+ setState(prev=> ({ ...prev, isColumnModalOpen: !prev.isColumnModalOpen }));
+ }, []);
if (isLoading) {
return