├── .eslintrc.json ├── .github └── FUNDING.yml ├── .gitignore ├── README.md ├── app ├── components │ ├── About │ │ ├── About.js │ │ ├── ScoreExplanation.js │ │ ├── SystemInfo.js │ │ └── TestProcedure.js │ ├── BrowserBarChart.js │ ├── BrowserCard.js │ ├── BrowserDetailsModal.js │ ├── BrowserRankingList.js │ ├── DarkModeToggle.js │ ├── Explanation.js │ ├── Footer.js │ ├── Header.js │ ├── Newsletter.js │ └── StickyAnnouncement.js ├── favicon.ico ├── globals.css ├── layout.js ├── lib │ └── getBrowsers.js ├── page.js └── privacy │ └── page.js ├── jsconfig.json ├── next.config.mjs ├── package-lock.json ├── package.json ├── postcss.config.mjs ├── public ├── data │ ├── android.json │ ├── browsers.json │ ├── ipad.json │ ├── macos-arm.json │ ├── macos-intel.json │ ├── raw-data.xlsx │ └── windows.json └── images │ ├── BlueskyLogo.png │ ├── RedditLogo.svg │ ├── TelegramLogo.svg │ ├── TwitterLogo.svg │ ├── browser-logos │ ├── arc.png │ ├── brave.png │ ├── bravebeta.webp │ ├── bravenightly.webp │ ├── chrome-canary.png │ ├── chrome.png │ ├── chromebeta.png │ ├── chromedev.webp │ ├── cromite.png │ ├── ddg.jpg │ ├── deta-surf.png │ ├── dia.png │ ├── edge-beta.png │ ├── edge-canary.png │ ├── edge.png │ ├── firefox-beta.png │ ├── firefox-dev.png │ ├── firefox-nightly.png │ ├── firefox.png │ ├── floorp.png │ ├── ghostery.png │ ├── iceraven.png │ ├── kito.png │ ├── kiwi.png │ ├── librewolf.svg │ ├── mull.png │ ├── mullvad.png │ ├── opera-beta.webp │ ├── opera-gx.png │ ├── opera-mini.png │ ├── opera.png │ ├── orion.png │ ├── quetta.png │ ├── safari.png │ ├── samsung-internet-beta.png │ ├── samsung-internet.png │ ├── sigmaos.png │ ├── soul.png │ ├── strawberry.jpeg │ ├── thorium.webp │ ├── tor.png │ ├── via.png │ ├── vivaldi-snapshot.png │ ├── vivaldi.png │ ├── waterfox.png │ ├── yandex-alpha.png │ ├── yandex-beta.webp │ ├── yandex.png │ ├── zen-twilight.png │ └── zen.svg │ └── logo.png └── tailwind.config.js /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: kawaiier 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 12 | polar: # Replace with a single Polar username 13 | buy_me_a_coffee: # Replace with a single Buy Me a Coffee username 14 | thanks_dev: # Replace with a single thanks.dev username 15 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | .aider* 38 | .env 39 | .cursor/ 40 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Browserating 2 | 3 | Browserating is a Next.js web application that provides performance rankings and comparisons for macOS browsers. It uses data from Speedometer 3.1 benchmarks to give users a comprehensive view of browser performance. 4 | 5 | ## Features 6 | 7 | - Responsive design for optimal viewing on various devices 8 | - Display a ranking list of macOS browsers based on performance metrics 9 | - Filter browsers based on their engine 10 | - Detailed information for each browser, including multiple versions 11 | - Privacy-focused with dedicated privacy page 12 | 13 | ## Technologies Used 14 | 15 | - Next.js 14.2.6 (App Router) 16 | - React 18 17 | - Tailwind CSS 3.4.1 18 | - Chart.js 4.4.6 with react-chartjs-2 19 | - Lucide React for icons 20 | - Vercel Analytics 21 | - JSON for data storage 22 | 23 | ## Setup and Installation 24 | 25 | 1. Clone the repository: 26 | 27 | ``` 28 | git clone https://github.com/kawaiier/browserating.git 29 | cd browserating 30 | ``` 31 | 32 | 2. Install dependencies: 33 | 34 | ``` 35 | npm install 36 | ``` 37 | 38 | 3. Run the development server: 39 | 40 | ``` 41 | npm run dev 42 | ``` 43 | 44 | 4. Open [http://localhost:3000](http://localhost:3000) in your browser to see the application. 45 | 46 | ## Data Management 47 | 48 | Browser data is stored in `public/data/`. To update browser information: 49 | 50 | 1. Open `platform.json` in a text editor 51 | 2. Modify the JSON data following the existing structure 52 | 3. Save the file 53 | 54 | The application will automatically reflect the changes on the next load. 55 | 56 | ## Contributing 57 | 58 | Contributions are welcome! Please feel free to submit a Pull Request. 59 | 60 | ## Acknowledgments 61 | 62 | - Speedometer 3.1 for providing benchmark data - https://browserbench.org/Speedometer3.1/ 63 | - Creator of AdBlock Tester - https://adblock-tester.com/ 64 | - All browser developers for their continuous work on improving web technologies 65 | 66 | ## Say Thanks 67 | 68 | If you find this project useful, please consider supporting me with a coffee. 69 | 70 | [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/J3J8TMWMG) 71 | 72 | ### Donation Addresses 73 | 74 | - **BTC**: `bc1qfyad27catyr8rtdhhydn8ummf996kxtesuw4hr` 75 | - **XMR**: `41zM5Hk39icMLDnbAckLpJHMwMPQKAQEADYA1AvjoZw9Y9NC7atnubrWPZKXWRbpZeGg66DkstQmA1oPZurRBcvRFbQ3PLs` 76 | - **LTC**: `ltc1qnqldulnxsxpz4g89uklsepjeqx7cajynzyr7tc` 77 | - **MATIC**: `0x6c056E9ccB183c08e9248eAF26160B5793221513` 78 | 79 | ## Star History 80 | 81 | [![Star History Chart](https://api.star-history.com/svg?repos=kawaiier/browserating&type=Date)](https://star-history.com/#kawaiier/browserating&Date) 82 | -------------------------------------------------------------------------------- /app/components/About/About.js: -------------------------------------------------------------------------------- 1 | import ScoreExplanation from "./ScoreExplanation"; 2 | import SystemInfo from "./SystemInfo"; 3 | import TestProcedure from "./TestProcedure"; 4 | 5 | export default function About() { 6 | return ( 7 |
11 |

15 | About Our Testing Methodology 16 |

17 | 18 |
19 | 23 | All browsers have been tested on a{" "} 24 | 25 | clean install of macOS Sequoia 15.4.{" "} 26 | 27 | The testing was conducted on a{" "} 28 | 29 | 14-inch MacBook Pro from 2023 30 | 31 | , equipped with an{" "} 32 | 33 | M3 Pro processor 34 | {" "} 35 | and{" "} 36 | 37 | 36 GB of RAM 38 | 39 | 40 | } 41 | /> 42 | 46 | All browsers have been tested on a{" "} 47 | 48 | clean install of macOS Ventura 13.6.9. 49 | 50 | The testing was conducted on a{" "} 51 | 52 | 15-inch MacBook Pro from 2019 53 | 54 | , equipped with a{" "} 55 | 56 | 2.3 GHz 8-core Intel Core i9 processor 57 | {" "} 58 | and{" "} 59 | 60 | 16 GB of RAM 61 | 62 | 63 | } 64 | /> 65 | 69 | All browsers have been tested on an{" "} 70 | 71 | old install of Windows 10 Pro. 72 | 73 | The testing was conducted on a{" "} 74 | 75 | Lenovo Ideapad Gaming 3 Laptop 76 | 77 | , equipped with an{" "} 78 | 79 | AMD Ryzen 5 5600H with Radeon Graphics 3.3 GHz processor 80 | {" "} 81 | and{" "} 82 | 83 | 16 GB of RAM 84 | 85 | 86 | } 87 | /> 88 | 92 | All browsers have been tested on{" "} 93 | 94 | Nothing OS 2.6 Android 14. 95 | 96 | The test was conducted on a{" "} 97 | 98 | Nothing Phone (2a) 99 | 100 | , equipped with an{" "} 101 | 102 | Dimensity 7200 Pro CPU with Mali-G610 MC4 GPU 103 | {" "} 104 | and{" "} 105 | 106 | 8 GB of RAM 107 | 108 | 109 | } 110 | /> 111 | 115 | All browsers have been tested on{" "} 116 | 117 | iPadOS 18.5. 118 | 119 | The test was conducted on a{" "} 120 | 121 | iPad Mini 7th Generation 122 | 123 | , equipped with a{" "} 124 | 125 | A17 Pro CPU 126 | {" "} 127 | and{" "} 128 | 129 | 8 GB of RAM 130 | 131 | 132 | } 133 | /> 134 |
135 |

136 | Testing Procedure 137 |

138 | 139 |
140 |
141 |
142 |

143 | Understanding the Scores 144 |

145 | 146 |
147 |
148 |
149 | ); 150 | } 151 | -------------------------------------------------------------------------------- /app/components/About/ScoreExplanation.js: -------------------------------------------------------------------------------- 1 | export default function ScoreExplanation() { 2 | return ( 3 |
4 |

5 | 6 | The higher the score, the faster the browser. 7 | {" "} 8 | Speedometer 3.1 measures browser performance by simulating user 9 | interactions on various web applications. 10 |

11 | 12 |
13 |

14 | Score Interpretation: 15 |

16 | 30 |
31 | 32 |

33 | Note: Performance may vary based on your specific hardware, operating 34 | system version, and browser configuration. 35 |

36 |
37 | ); 38 | } 39 | -------------------------------------------------------------------------------- /app/components/About/SystemInfo.js: -------------------------------------------------------------------------------- 1 | export default function SystemInfo({ title, details }) { 2 | return ( 3 |
4 | 24 |
25 | {details} 26 |
27 |
28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /app/components/About/TestProcedure.js: -------------------------------------------------------------------------------- 1 | export default function TestProcedure() { 2 | return ( 3 |
4 |
5 |
6 | 11 | 16 | 17 |
18 |

19 | For Speedometer benchmark, for each browser{" "} 20 | 21 | five tests were conducted 22 | 23 | . The{" "} 24 | 25 | best and worst results were eliminated 26 | 27 | , and the{" "} 28 | 29 | average of the remaining three tests was calculated 30 | {" "} 31 | to determine the final result. 32 |

33 |
34 | 35 |
36 |
37 | 42 | 47 | 48 |
49 |

50 | For RAM usage test,{" "} 51 | 52 | the cumulative memory consumption was measured 53 | {" "} 54 | after sequentially loading seven diverse websites: IGN, ESPN, Figma, 55 | Britannica, Wired, Bloomberg, and Reddit's popular page. 56 | Measurements were taken using Activity Monitor, filtered by each 57 | browser's name. 58 |

59 |
60 | 61 |
62 |
63 | 68 | 73 | 74 |
75 |

76 | For adblock test,{" "} 77 | 84 | AdBlock Tester 85 | {" "} 86 | website was used. 87 |

88 |
89 |
90 | ); 91 | } 92 | -------------------------------------------------------------------------------- /app/components/BrowserBarChart.js: -------------------------------------------------------------------------------- 1 | import { useMemo } from "react"; 2 | 3 | export default function BrowserBarChart({ 4 | browsers, 5 | platform, 6 | getEngineColor, 7 | }) { 8 | const chartData = useMemo(() => { 9 | if (!browsers || !platform) return []; 10 | 11 | return browsers 12 | .filter((browser) => { 13 | const platformData = browser[platform]; 14 | const scores = platformData?.versions?.[0]?.scores; 15 | return scores && typeof scores.speedometer3 === "number"; 16 | }) 17 | .map((browser) => { 18 | const platformData = browser[platform]; 19 | return { 20 | name: browser.name, 21 | score: platformData.versions[0].scores.speedometer3, 22 | engine: platformData.engine || "unknown", 23 | }; 24 | }) 25 | .sort((a, b) => a.score - b.score); 26 | }, [browsers, platform]); 27 | 28 | const maxScore = useMemo(() => { 29 | return Math.max(...chartData.map((item) => item.score), 1); 30 | }, [chartData]); 31 | 32 | if (!browsers || !platform || !getEngineColor) { 33 | console.warn("BrowserBarChart: Missing required props"); 34 | return null; 35 | } 36 | 37 | if (chartData.length === 0) { 38 | return ( 39 |
44 | No performance data available for the selected platform 45 |
46 | ); 47 | } 48 | 49 | // Get platform display name 50 | const platformNames = { 51 | "macos-arm": "macOS ARM", 52 | "macos-intel": "macOS Intel", 53 | windows: "Windows", 54 | android: "Android", 55 | ipad: "iPad OS", 56 | }; 57 | 58 | const platformName = platformNames[platform] || platform; 59 | 60 | return ( 61 |
62 |
63 |

64 | Performance Comparison - {platformName} 65 |

66 |

67 | Scroll to view all browsers 68 | Swipe to view 69 |

70 |
71 | 72 |
78 |
79 | {chartData.map((item, index) => { 80 | const percentHeight = (item.score / maxScore) * 100; 81 | const scoreFormatted = item.score.toLocaleString(undefined, { 82 | maximumFractionDigits: 1, 83 | }); 84 | 85 | return ( 86 |
91 | 97 | 98 |
108 |
109 |
117 | {/* Hover Score */} 118 |
119 | {scoreFormatted} 120 |
121 |
122 |
123 |
124 | 125 |
126 |
130 | {item.name} 131 |
132 |
133 |
134 | ); 135 | })} 136 |
137 |
138 | 139 | {/* Screen reader only summary */} 140 |
141 |

Summary of browser performance on {platformName}

142 |

From highest to lowest score:

143 |
    144 | {[...chartData].reverse().map((item, index) => ( 145 |
  1. 146 | {item.name} with a score of {item.score.toFixed(1)} using{" "} 147 | {item.engine} engine 148 |
  2. 149 | ))} 150 |
151 |
152 | 153 | 185 |
186 | ); 187 | } 188 | -------------------------------------------------------------------------------- /app/components/BrowserCard.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | 3 | import BrowserDetailsModal from "./BrowserDetailsModal"; 4 | import Image from "next/image"; 5 | 6 | const BrowserCard = React.memo( 7 | ({ browser, getEngineColor, rank, selectedPlatform }) => { 8 | const [showModal, setShowModal] = useState(false); 9 | 10 | const platformData = browser[selectedPlatform]; 11 | if ( 12 | !platformData || 13 | !platformData.versions || 14 | platformData.versions.length === 0 15 | ) 16 | return null; 17 | 18 | const latestVersion = platformData.versions[0]; 19 | const prevSpeedometer3Score = 20 | platformData.versions.length > 1 21 | ? platformData.versions[1].scores.speedometer3 22 | : null; 23 | 24 | // Use the platform-specific engine 25 | const platformEngine = platformData.engine; 26 | 27 | const getRankStyle = (rank) => { 28 | switch (rank) { 29 | case 1: 30 | return "ring-4 ring-yellow-400"; // Gold 31 | case 2: 32 | return "ring-4 ring-gray-300"; // Silver 33 | case 3: 34 | return "ring-4 ring-amber-600"; // Bronze 35 | default: 36 | return ""; 37 | } 38 | }; 39 | 40 | // Calculate score difference for accessibility text 41 | const scoreDifference = prevSpeedometer3Score 42 | ? latestVersion.scores.speedometer3 - prevSpeedometer3Score 43 | : null; 44 | 45 | const scoreChangeText = scoreDifference 46 | ? `${scoreDifference > 0 ? "Increased" : "Decreased"} by ${Math.abs( 47 | scoreDifference 48 | ).toFixed(2)} points from previous version` 49 | : ""; 50 | 51 | return ( 52 | <> 53 |
setShowModal(true)} 68 | onKeyDown={(e) => { 69 | if (e.key === "Enter" || e.key === " ") { 70 | e.preventDefault(); 71 | setShowModal(true); 72 | } 73 | }} 74 | tabIndex={ 75 | selectedPlatform === "android" && platformData.versions.length === 1 76 | ? -1 77 | : 0 78 | } 79 | > 80 | {selectedPlatform === "android" && 81 | platformData.versions.length === 1 && ( 82 |
83 | Outdated 84 |
85 | )} 86 |
87 |
88 | {`${browser.name} 96 |

100 | e.stopPropagation()} 107 | aria-label={`Visit ${browser.name} website`} 108 | > 109 | {browser.name} 110 | 111 |

112 |
113 |
114 | 118 | {latestVersion.version} 119 | 120 | 125 | {platformEngine} 126 | 127 |
128 |
129 | {/* Speedometer Score */} 130 |
131 |

132 | Speedometer 3.1 133 |

134 |
135 |

136 | {latestVersion.scores.speedometer3.toFixed(2)} 137 |

138 | {prevSpeedometer3Score && ( 139 |

140 | 144 | 0 145 | ? "text-green-600 dark:text-green-400" 146 | : latestVersion.scores.speedometer3 - 147 | prevSpeedometer3Score < 148 | 0 149 | ? "text-red-600 dark:text-red-400" 150 | : "text-gray-600 dark:text-gray-400" 151 | }`} 152 | > 153 | {latestVersion.scores.speedometer3 - 154 | prevSpeedometer3Score > 155 | 0 156 | ? "+" 157 | : ""} 158 | {( 159 | latestVersion.scores.speedometer3 - 160 | prevSpeedometer3Score 161 | ).toFixed(2)} 162 | 163 |

164 | )} 165 |
166 |
167 | 168 | {/* RAM Score */} 169 | {latestVersion.scores.ram && ( 170 |
171 |

172 | RAM Usage 173 |

174 |
175 |

176 | {latestVersion.scores.ram.toFixed(0)} 177 |

178 | 179 | MB 180 | 181 |
182 |
183 | )} 184 | 185 | {/* Adblock Score */} 186 | {latestVersion.scores.adblock && ( 187 |
188 |

189 | Adblock 190 |

191 |
192 |

193 | {latestVersion.scores.adblock.toFixed(0)}/100 194 |

195 |
202 |
206 |
207 |
208 |
209 | )} 210 |
211 |
212 |
213 | 214 | {showModal && ( 215 | setShowModal(false)} 219 | /> 220 | )} 221 | 222 | ); 223 | } 224 | ); 225 | 226 | // Add display name for the memoized component 227 | BrowserCard.displayName = "BrowserCard"; 228 | 229 | export default BrowserCard; 230 | -------------------------------------------------------------------------------- /app/components/BrowserDetailsModal.js: -------------------------------------------------------------------------------- 1 | import { 2 | BarElement, 3 | CategoryScale, 4 | Chart as ChartJS, 5 | Legend, 6 | LineElement, 7 | LinearScale, 8 | PointElement, 9 | Title, 10 | Tooltip, 11 | } from "chart.js"; 12 | 13 | import { Bar } from "react-chartjs-2"; 14 | import Image from "next/image"; 15 | 16 | ChartJS.register( 17 | CategoryScale, 18 | LinearScale, 19 | PointElement, 20 | LineElement, 21 | Title, 22 | Tooltip, 23 | Legend, 24 | BarElement 25 | ); 26 | 27 | const BrowserDetailsModal = ({ browser, selectedPlatform, onClose }) => { 28 | const platformData = browser[selectedPlatform]; 29 | 30 | // Check if platform data exists and has the correct structure 31 | if ( 32 | !platformData || 33 | !platformData.versions || 34 | platformData.versions.length === 0 35 | ) { 36 | return ( 37 |
41 |
e.stopPropagation()} 44 | role="dialog" 45 | aria-modal="true" 46 | > 47 |
48 |

49 | No Data Available 50 |

51 |

52 | No performance data available for {browser.name} on this platform. 53 |

54 | 60 |
61 |
62 |
63 | ); 64 | } 65 | 66 | const latestVersion = platformData.versions[0]; 67 | const sortedData = [...platformData.versions].reverse(); 68 | const platformEngine = platformData.engine; 69 | 70 | const getEngineColor = (engine) => { 71 | switch (engine.toLowerCase()) { 72 | case "blink": 73 | return "bg-blue-100 dark:bg-sky-900 text-blue-800 dark:text-blue-100"; 74 | case "gecko": 75 | return "bg-green-100 dark:bg-emerald-900 text-green-800 dark:text-green-100"; 76 | case "webkit": 77 | return "bg-orange-100 dark:bg-amber-900 text-orange-800 dark:text-orange-100"; 78 | default: 79 | return "bg-gray-100 dark:bg-gray-700 text-gray-800 dark:text-gray-100"; 80 | } 81 | }; 82 | 83 | const chartData = { 84 | labels: sortedData.map((data) => data.version), 85 | datasets: [ 86 | { 87 | label: "Speedometer 3 Score", 88 | data: sortedData.map((data) => data.scores.speedometer3), 89 | backgroundColor: "#7853E0", 90 | borderColor: "#7853E0", 91 | tension: 0.1, 92 | }, 93 | ], 94 | }; 95 | 96 | const chartOptions = { 97 | maintainAspectRatio: false, 98 | scales: { 99 | x: { 100 | grid: { 101 | color: "rgba(120, 83, 224, 0.1)", 102 | }, 103 | ticks: { 104 | color: "#9CA3AF", // gray-400 105 | }, 106 | }, 107 | y: { 108 | grid: { 109 | color: "rgba(120, 83, 224, 0.1)", 110 | }, 111 | ticks: { 112 | color: "#9CA3AF", // gray-400 113 | }, 114 | }, 115 | }, 116 | plugins: { 117 | legend: { 118 | labels: { 119 | color: "#9CA3AF", // gray-400 120 | }, 121 | }, 122 | }, 123 | }; 124 | 125 | // Get platform display name 126 | const platformNames = { 127 | "macos-arm": "macOS ARM", 128 | "macos-intel": "macOS Intel", 129 | windows: "Windows", 130 | android: "Android", 131 | ipad: "iPad OS", 132 | }; 133 | 134 | const platformName = platformNames[selectedPlatform] || selectedPlatform; 135 | 136 | return ( 137 |
141 |
e.stopPropagation()} 144 | role="dialog" 145 | aria-modal="true" 146 | > 147 |
148 |
149 |
150 | {`${browser.name} 157 |
158 |

159 | {browser.name} 160 |

161 |

162 | Performance on {platformName} 163 |

164 |
165 |
166 |
167 | 172 | {platformEngine} 173 | 174 |
175 |
176 | 195 |
196 | 197 |
198 |

199 | Performance History on {platformName} 200 |

201 |
202 | 203 |
204 |
205 | 206 |
207 |
208 |

209 | Version History 210 |

211 |
212 | {platformData.versions.map((data, index) => ( 213 |
217 |
218 |
219 | 220 | Version {data.version} 221 | 222 | {index === 0 && ( 223 | 224 | Latest 225 | 226 | )} 227 |
228 | 229 | {data.scores.speedometer3.toFixed(2)} 230 | 231 |
232 | 233 | {/* Additional scores if available */} 234 | {(data.scores.ram || data.scores.adblock) && ( 235 |
236 | {data.scores.ram && ( 237 | RAM: {data.scores.ram.toFixed(0)} MB 238 | )} 239 | {data.scores.adblock && ( 240 | 241 | Adblock: {data.scores.adblock.toFixed(0)}/100 242 | 243 | )} 244 |
245 | )} 246 | 247 |
248 | {data.releaseDate && 249 | `Released: ${new Date( 250 | data.releaseDate 251 | ).toLocaleDateString()}`} 252 |
253 |
254 | ))} 255 |
256 |
257 |
258 |
259 |
260 | ); 261 | }; 262 | 263 | export default BrowserDetailsModal; 264 | -------------------------------------------------------------------------------- /app/components/BrowserRankingList.js: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useEffect, useMemo, useState } from "react"; 4 | 5 | import BrowserBarChart from "./BrowserBarChart"; 6 | import BrowserCard from "./BrowserCard"; 7 | import { getBrowsers } from "../lib/getBrowsers"; 8 | 9 | const engineColors = { 10 | Blink: 11 | "bg-blue-100 dark:bg-sky-900 text-blue-800 dark:text-blue-100 hover:bg-blue-200 dark:hover:bg-sky-800", 12 | Gecko: 13 | "bg-green-100 dark:bg-emerald-900 text-green-800 dark:text-green-100 hover:bg-green-200 dark:hover:bg-emerald-800", 14 | WebKit: 15 | "bg-orange-100 dark:bg-amber-900 text-orange-800 dark:text-orange-100 hover:bg-orange-200 dark:hover:bg-amber-800", 16 | All: "bg-gray-100 dark:bg-gray-700 text-gray-800 dark:text-gray-100 hover:bg-gray-200 dark:hover:bg-gray-600", 17 | }; 18 | 19 | const getEngineColor = (engine) => { 20 | return ( 21 | engineColors[engine] || 22 | "bg-red-100 dark:bg-red-900 text-red-800 dark:text-red-100 hover:bg-red-200 dark:hover:bg-red-800" 23 | ); 24 | }; 25 | 26 | const platformNames = { 27 | "macos-arm": "macOS ARM", 28 | "macos-intel": "macOS Intel", 29 | windows: "Windows", 30 | android: "Android", 31 | ipad: "iPad OS", 32 | }; 33 | 34 | // Add this new constant for the NEW badge 35 | const NEW_PLATFORM = "android"; 36 | 37 | // Skeleton Loader Component 38 | const SkeletonLoader = () => ( 39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 | ); 65 | 66 | export default function BrowserRankingList() { 67 | const [browsers, setBrowsers] = useState([]); 68 | const [filteredBrowsers, setFilteredBrowsers] = useState([]); 69 | const [selectedEngine, setSelectedEngine] = useState("All"); 70 | const [selectedPlatform, setSelectedPlatform] = useState("macos-arm"); 71 | const [isLoading, setIsLoading] = useState(true); 72 | const [error, setError] = useState(null); 73 | const [selectedBrowsers, setSelectedBrowsers] = useState([]); 74 | 75 | useEffect(() => { 76 | async function fetchBrowsers() { 77 | try { 78 | const data = await getBrowsers(); 79 | setBrowsers(data); 80 | setFilteredBrowsers(data); 81 | setIsLoading(false); 82 | } catch (err) { 83 | setError("Failed to load browser data"); 84 | setIsLoading(false); 85 | } 86 | } 87 | fetchBrowsers(); 88 | }, []); 89 | 90 | const sortBrowsersByPlatform = (browsers, platform) => { 91 | return browsers.sort((a, b) => { 92 | const aScore = a[platform]?.versions?.[0]?.scores?.speedometer3 || 0; 93 | const bScore = b[platform]?.versions?.[0]?.scores?.speedometer3 || 0; 94 | return bScore - aScore; 95 | }); 96 | }; 97 | 98 | const sortedBrowsers = useMemo( 99 | () => sortBrowsersByPlatform(browsers, selectedPlatform), 100 | [browsers, selectedPlatform] 101 | ); 102 | 103 | useEffect(() => { 104 | const filtered = 105 | selectedEngine === "All" 106 | ? sortedBrowsers 107 | : sortedBrowsers.filter((browser) => { 108 | const platformData = browser[selectedPlatform]; 109 | return platformData?.engine === selectedEngine; 110 | }); 111 | setFilteredBrowsers(filtered); 112 | }, [selectedEngine, sortedBrowsers, selectedPlatform]); 113 | 114 | const engines = useMemo(() => { 115 | const platformEngines = browsers 116 | .filter((browser) => browser[selectedPlatform]) 117 | .map((browser) => browser[selectedPlatform].engine) 118 | .filter(Boolean); 119 | 120 | return ["All", ...new Set(platformEngines)]; 121 | }, [browsers, selectedPlatform]); 122 | 123 | const platforms = ["macos-intel", "macos-arm", "windows", "android", "ipad"]; 124 | 125 | const handleEngineFilter = (engine) => { 126 | setSelectedEngine(engine); 127 | }; 128 | 129 | const handlePlatformChange = (platform) => { 130 | setSelectedPlatform(platform); 131 | setSelectedEngine("All"); // Reset engine filter when platform changes 132 | }; 133 | 134 | const handleBrowserSelect = (browser) => { 135 | setSelectedBrowsers((prev) => { 136 | const exists = prev.find((b) => b.name === browser.name); 137 | if (exists) { 138 | return prev.filter((b) => b.name !== browser.name); 139 | } 140 | return [...prev, browser]; 141 | }); 142 | }; 143 | 144 | const renderPlatformButtons = () => ( 145 |
150 | {platforms.map((platform) => ( 151 | 178 | ))} 179 |
180 | ); 181 | 182 | const renderEngineButtons = () => ( 183 |
188 | {engines.map((engine) => ( 189 | 203 | ))} 204 |
205 | ); 206 | 207 | if (isLoading) { 208 | return ( 209 |
210 |

Browser Performance Rankings

211 | {renderPlatformButtons()} 212 | {renderEngineButtons()} 213 |
218 |
Loading browser data...
219 | {Array(6) 220 | .fill(0) 221 | .map((_, i) => ( 222 | 223 | ))} 224 |
225 |
226 | ); 227 | } 228 | 229 | if (error) { 230 | return ( 231 |
232 |

233 | {error} 234 |

235 | 241 |
242 | ); 243 | } 244 | 245 | return ( 246 |
247 |

Browser Performance Rankings

248 | {renderPlatformButtons()} 249 | {renderEngineButtons()} 250 | 251 | {/* Browser Cards */} 252 |
256 | {filteredBrowsers.length === 0 ? ( 257 |

258 | No browsers match the selected filters. 259 |

260 | ) : ( 261 | filteredBrowsers.map((browser, index) => ( 262 | 270 | )) 271 | )} 272 |
273 | 274 | {/* Chart Section */} 275 |
276 | 281 |
282 |
283 | ); 284 | } 285 | -------------------------------------------------------------------------------- /app/components/DarkModeToggle.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export default function DarkModeToggle({ darkMode, toggleDarkMode }) { 4 | // Pre-calculate star positions to avoid repositioning on every render 5 | const stars = Array.from({ length: 6 }).map((_, i) => ({ 6 | top: Math.random() * 100, 7 | left: Math.random() * 100, 8 | delay: Math.random() * 1500, 9 | key: `star-${i}`, 10 | })); 11 | 12 | return ( 13 | 84 | ); 85 | } 86 | -------------------------------------------------------------------------------- /app/components/Explanation.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | // Collapsible section component similar to SystemInfo 4 | const ExplanationSection = ({ title, children }) => { 5 | return ( 6 |
7 | 27 |
28 | {children} 29 |
30 |
31 | ); 32 | }; 33 | 34 | export default function Explanation() { 35 | return ( 36 |
40 |

44 | Understanding Browser Performance Rankings 45 |

46 | 47 |
48 |

49 | BrowseRating began as a curiosity-driven project to quantify the 50 | perceived speed differences between browsers. What started as a 51 | personal investigation has evolved into a{" "} 52 | 53 | comprehensive benchmarking platform comparing browser performance 54 | {" "} 55 | through synthetic testing methodologies. 56 |

57 | 58 | 59 |

60 | BrowseRating conducts controlled performance tests on identical 61 | hardware configurations, ensuring fair comparisons between browsers. 62 | Each browser undergoes the same synthetic tests, with detailed 63 | system specifications displayed alongside the results for complete 64 | transparency. While these benchmarks focus on DOM element creation 65 | speed, they provide valuable insights into browser capabilities 66 | across different platforms. 67 |

68 |
69 | 70 | 71 |

72 | It's important to note that while these benchmarks provide 73 | valuable comparative data, the performance differences may not be 74 | immediately noticeable in everyday browsing. Most modern browsers 75 | deliver excellent performance for typical usage, with factors like 76 | internet connectivity often having a more significant impact on user 77 | experience than rendering differences. 78 |

79 |
80 | 81 | 82 |
83 | {[ 84 | "Provides objective performance comparisons across browsers", 85 | "Helps track browser optimization progress over time", 86 | "Offers insights into platform-specific performance variations", 87 | "Supports informed decision-making for power users", 88 | ].map((item, index) => ( 89 |
90 |
91 | 96 | 101 | 102 |
103 |

104 | {item} 105 |

106 |
107 | ))} 108 |
109 |
110 | 111 | 112 |

113 | To provide a more comprehensive comparison, we've included 114 | average RAM usage measurements for browsers with a single active 115 | YouTube tab. Keep in mind that actual memory consumption can vary 116 | significantly based on several factors, including: 117 |

118 |
119 | {[ 120 | "Number and types of installed extensions/add-ons", 121 | "Other open tabs and their content", 122 | "Background applications and system load", 123 | "Operating system memory management", 124 | ].map((item, index) => ( 125 |
126 |
127 | 132 | 137 | 138 |
139 |

140 | {item} 141 |

142 |
143 | ))} 144 |
145 |
146 | 147 | 148 |

149 | Built-in ad blocking capabilities have become increasingly important 150 | for modern browsers as they directly impact both performance and 151 | user privacy. By blocking intrusive advertisements and tracking 152 | scripts at the browser level, users can experience{" "} 153 | 154 | faster page load times, reduced bandwidth usage, and better 155 | overall browsing security 156 | {" "} 157 | without relying on third-party extensions. Native ad blocking also 158 | tends to be more efficient than extension-based solutions, consuming 159 | fewer system resources while providing comprehensive protection 160 | against unwanted content and potential security threats that could 161 | be delivered through ad networks. 162 |

163 |
164 | 165 | 166 |

167 | We're committed to expanding our benchmark suite and 168 | maintaining up-to-date comparisons. While our current scores are 169 | pending updates due to technical considerations, we're 170 | preparing to conduct a comprehensive round of testing across all 171 | major browsers in the coming weeks. Stay tuned for fresh performance 172 | insights across{" "} 173 | 174 | multiple browser versions 175 | 176 | . 177 |

178 |
179 |
180 |
181 | ); 182 | } 183 | -------------------------------------------------------------------------------- /app/components/Footer.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | 3 | import Image from "next/image"; 4 | 5 | const DONATION_ADDRESSES = { 6 | BTC: "bc1qfyad27catyr8rtdhhydn8ummf996kxtesuw4hr", 7 | XMR: "41zM5Hk39icMLDnbAckLpJHMwMPQKAQEADYA1AvjoZw9Y9NC7atnubrWPZKXWRbpZeGg66DkstQmA1oPZurRBcvRFbQ3PLs", 8 | LTC: "ltc1qnqldulnxsxpz4g89uklsepjeqx7cajynzyr7tc", 9 | MATIC: "0x6c056E9ccB183c08e9248eAF26160B5793221513", 10 | }; 11 | 12 | export default function Footer() { 13 | const [isCopied, setIsCopied] = useState(false); 14 | const [copiedCurrency, setCopiedCurrency] = useState(""); 15 | const [isMobile, setIsMobile] = useState(false); 16 | 17 | useEffect(() => { 18 | // Check if device is mobile 19 | const checkMobile = () => { 20 | setIsMobile(window.innerWidth < 768); 21 | }; 22 | 23 | checkMobile(); 24 | window.addEventListener("resize", checkMobile); 25 | 26 | return () => window.removeEventListener("resize", checkMobile); 27 | }, []); 28 | 29 | const handleCopy = (text, currency) => { 30 | navigator.clipboard.writeText(text); 31 | setIsCopied(true); 32 | setCopiedCurrency(currency); 33 | setTimeout(() => { 34 | setIsCopied(false); 35 | setCopiedCurrency(""); 36 | }, 2000); 37 | }; 38 | 39 | const truncateAddress = (address, type) => { 40 | if (isMobile) { 41 | if (type === "XMR") { 42 | return `${address.slice(0, 10)}...${address.slice(-10)}`; 43 | } 44 | return `${address.slice(0, 8)}...${address.slice(-8)}`; 45 | } 46 | 47 | if (type === "XMR") { 48 | return `${address.slice(0, 20)}...${address.slice(-20)}`; 49 | } 50 | return address; 51 | }; 52 | 53 | return ( 54 | 235 | ); 236 | } 237 | -------------------------------------------------------------------------------- /app/components/Header.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | 3 | import DarkModeToggle from "./DarkModeToggle"; 4 | import Image from "next/image"; 5 | 6 | export default function Header({ darkMode, toggleDarkMode }) { 7 | return ( 8 |
12 | {/* Background Pattern */} 13 | 44 | 45 | {/* Content Container */} 46 |
47 |
48 | {/* Logo Section */} 49 |
50 |
51 |
52 |
53 | 54 | Browserating Logo 62 | 63 |
64 |
65 |
66 | 67 | {/* Text Section */} 68 |
69 |

70 | BrowseRating 71 |

72 |

73 | Browser Performance Ranking for 74 | 75 | 76 | macOS / Windows / Android / iPad 77 | 78 | 79 |

80 |

81 | The score displayed below reflects the browser's performance 82 | in the Speedometer 3.1 benchmark. The higher the score, the 83 | better. 84 |

85 |
86 |

87 | Last updated: 88 |

89 |
90 |
91 |

92 | The rating is updated monthly. If you would like weekly updates, 93 | consider supporting the project. Your support will motivate me 94 | to dedicate time each week to conduct these tests. 95 |

96 |
97 |
98 |
99 |
100 |
101 | 102 |
103 |
104 | ); 105 | } 106 | -------------------------------------------------------------------------------- /app/components/Newsletter.js: -------------------------------------------------------------------------------- 1 | import { useRef, useState } from "react"; 2 | 3 | export default function Newsletter() { 4 | const [email, setEmail] = useState(""); 5 | const [isSubmitting, setIsSubmitting] = useState(false); 6 | const [submitStatus, setSubmitStatus] = useState(null); 7 | const formRef = useRef(null); 8 | 9 | const handleSubmit = async (e) => { 10 | e.preventDefault(); 11 | 12 | if (!email) return; 13 | 14 | setIsSubmitting(true); 15 | setSubmitStatus(null); 16 | 17 | // Simulate form submission 18 | try { 19 | // Replace with actual API call when ready 20 | await new Promise((resolve) => setTimeout(resolve, 1000)); 21 | setSubmitStatus({ success: true, message: "Thank you for subscribing!" }); 22 | setEmail(""); 23 | } catch (error) { 24 | setSubmitStatus({ 25 | success: false, 26 | message: "Something went wrong. Please try again.", 27 | }); 28 | } finally { 29 | setIsSubmitting(false); 30 | } 31 | }; 32 | 33 | return ( 34 |
38 |
39 |

43 | Stay Updated with Browser Performance 44 |

45 |

46 | Get notified when we publish new browser performance comparisons 47 |

48 |
49 | 50 |
56 |
57 |
58 | 61 | setEmail(e.target.value)} 66 | placeholder="Enter your email" 67 | className="w-full px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 68 | bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 69 | placeholder-gray-500 dark:placeholder-gray-400 70 | focus:outline-none focus:ring-2 focus:ring-purple-500 dark:focus:ring-purple-400 71 | transition-colors duration-200" 72 | required 73 | aria-required="true" 74 | aria-invalid={email && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)} 75 | aria-describedby={submitStatus ? "form-status" : undefined} 76 | /> 77 |
78 | 91 |
92 | 93 | {submitStatus && ( 94 |
104 | {submitStatus.message} 105 |
106 | )} 107 | 108 |

109 | We respect your privacy. Unsubscribe at any time. 110 |

111 |
112 |
113 | ); 114 | } 115 | -------------------------------------------------------------------------------- /app/components/StickyAnnouncement.js: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React, { useState } from "react"; 4 | 5 | const StickyAnnouncement = () => { 6 | const [isVisible, setIsVisible] = useState(true); 7 | 8 | if (!isVisible) { 9 | return null; 10 | } 11 | 12 | return ( 13 |
14 |

15 | Subscribe to /r/aiBrowsing — A place 16 | for discussing browsers and extensions that incorporate AI features 17 |

18 |
19 | 25 | Subscribe 26 | 27 | 34 |
35 |
36 | ); 37 | }; 38 | 39 | export default StickyAnnouncement; 40 | -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kawaiier/browserating/b1c8832955fc37f3eb02d5265663679276447690/app/favicon.ico -------------------------------------------------------------------------------- /app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @keyframes fade-in { 6 | 0% { 7 | opacity: 0; 8 | transform: scale(0.95); /* Optional: Slightly scale down */ 9 | } 10 | 100% { 11 | opacity: 1; 12 | transform: scale(1); /* Return to normal scale */ 13 | } 14 | } 15 | 16 | .fade-in { 17 | animation: fade-in 0.4s ease-in-out forwards; /* Adjust duration and easing as needed */ 18 | } 19 | 20 | @keyframes twinkle { 21 | 0%, 22 | 100% { 23 | opacity: 1; 24 | } 25 | 50% { 26 | opacity: 0.3; 27 | } 28 | } 29 | 30 | .animate-twinkle { 31 | animation: twinkle 2s ease-in-out infinite; 32 | } 33 | 34 | @keyframes bounce-gentle { 35 | 0%, 36 | 100% { 37 | transform: translateY(0); 38 | } 39 | 50% { 40 | transform: translateY(-5px); 41 | } 42 | } 43 | 44 | .animate-bounce-gentle { 45 | animation: bounce-gentle 2s ease-in-out infinite; 46 | } 47 | 48 | /* Improved focus styles for better accessibility */ 49 | :focus-visible { 50 | outline: 2px solid #7853e0; 51 | outline-offset: 2px; 52 | border-radius: 0.25rem; 53 | } 54 | 55 | /* Ensure interactive elements have proper focus states */ 56 | a:focus-visible, 57 | button:focus-visible, 58 | input:focus-visible, 59 | select:focus-visible, 60 | textarea:focus-visible, 61 | [tabindex]:focus-visible { 62 | outline: 2px solid #7853e0; 63 | outline-offset: 2px; 64 | box-shadow: 0 0 0 4px rgba(120, 83, 224, 0.2); 65 | } 66 | 67 | /* Dark mode focus styles */ 68 | .dark a:focus-visible, 69 | .dark button:focus-visible, 70 | .dark input:focus-visible, 71 | .dark select:focus-visible, 72 | .dark textarea:focus-visible, 73 | .dark [tabindex]:focus-visible { 74 | outline-color: #9b7be8; 75 | box-shadow: 0 0 0 4px rgba(155, 123, 232, 0.2); 76 | } 77 | -------------------------------------------------------------------------------- /app/layout.js: -------------------------------------------------------------------------------- 1 | import "./globals.css"; 2 | 3 | import { Analytics } from "@vercel/analytics/next"; 4 | import { Inter } from "next/font/google"; 5 | import Script from "next/script"; 6 | import StickyAnnouncement from "./components/StickyAnnouncement"; 7 | 8 | const inter = Inter({ subsets: ["latin"] }); 9 | 10 | export const metadata = { 11 | title: "BrowseRating - Browser Performance for macOS, Windows and Android", 12 | description: 13 | "Compare performance of macOS, Windows and Android browsers based on Speedometer 3 benchmark results, adblocking quality, and RAM usage. Find the fastest and most efficient browsers for your device.", 14 | keywords: 15 | "browser performance, browser benchmark, Speedometer 3, browser comparison, fastest browser, macOS browser, Windows browser, Android browser, adblocking quality, RAM usage", 16 | authors: [{ name: "Sergei Manvelov" }], 17 | openGraph: { 18 | title: "BrowseRating - Browser Performance Comparison", 19 | description: 20 | "Compare browser performance across macOS, Windows and Android based on Speedometer 3 benchmark results, adblocking quality, and RAM usage.", 21 | url: "https://browserating.com", 22 | siteName: "BrowseRating", 23 | locale: "en_US", 24 | type: "website", 25 | }, 26 | twitter: { 27 | card: "summary_large_image", 28 | title: "BrowseRating - Browser Performance Comparison", 29 | description: 30 | "Compare browser performance across macOS, Windows and Android based on Speedometer 3 benchmark results, adblocking quality, and RAM usage.", 31 | creator: "@kawaiier101", 32 | }, 33 | robots: { 34 | index: true, 35 | follow: true, 36 | }, 37 | }; 38 | 39 | export default function RootLayout({ children }) { 40 | return ( 41 | 42 | 43 | 44 | 45 | 46 | 57 | 74 | 75 | 76 | {children} 77 | 78 | 79 |