From dcd0b6d6938cf058a4a5e9fa53c7e2076ccdb95c Mon Sep 17 00:00:00 2001 From: Greg V Date: Mon, 20 Apr 2026 21:35:14 -0700 Subject: [PATCH 001/130] Perf: tier-1 bundle + third-party-script wins Initial PageSpeed Insights mobile perf score was 10 / desktop 41. The site was pulling the reCAPTCHA v3 script and full MUI imports on every page, plus wasted preconnect hints. - Scope GoogleReCaptchaProvider to pages that actually use reCAPTCHA. Previously wrapped the entire app in _app.js, forcing every visitor to download Google's reCAPTCHA v3 bundle (~200KB + a cross-origin script request) on the homepage, event pages, nonprofit pages, profile, etc. Now lives only around LeadForm, /contact, /nonprofits/apply, and the 5 application pages. - Add experimental.optimizePackageImports for MUI (material, icons, lab, x-data-grid, x-date-pickers), date-fns, lodash, react-icons. Next 16 transitively tree-shakes named imports from these packages, which should trim the main chunk noticeably. - Remove orphan Google Fonts preconnects from pages/index.js and hack/[event_id].js. The codebase declares Inter / Space Grotesk / Fira Code in CSS variables but never loads them via @font-face or @import, so the preconnects cost two DNS+TLS handshakes for no content. - Drop react-beautiful-dnd (deprecated) and migrate the sole usage (NonprofitRanker.js) to @hello-pangea/dnd, which is already a dependency and has an identical API. Removes ~8 transitive packages. Co-Authored-By: Claude Opus 4.7 (1M context) --- next.config.js | 17 ++++ package-lock.json | 98 +------------------ package.json | 1 - src/components/LeadForm/LeadForm.js | 12 ++- src/components/ReCaptchaProvider.js | 18 ++++ .../TeamCreation/NonprofitRanker.js | 2 +- src/pages/_app.js | 33 +++---- src/pages/contact/index.js | 9 +- src/pages/hack/[event_id].js | 11 --- .../hack/[event_id]/hacker-application.js | 9 +- .../hack/[event_id]/judge-application.js | 9 +- .../hack/[event_id]/mentor-application.js | 9 +- .../hack/[event_id]/sponsor-application.js | 9 +- .../hack/[event_id]/volunteer-application.js | 9 +- src/pages/index.js | 4 - src/pages/nonprofits/apply/index.js | 11 ++- 16 files changed, 121 insertions(+), 140 deletions(-) create mode 100644 src/components/ReCaptchaProvider.js diff --git a/next.config.js b/next.config.js index 1fa3d4bf..308beff6 100644 --- a/next.config.js +++ b/next.config.js @@ -8,6 +8,23 @@ module.exports = { removeConsole: process.env.NODE_ENV === "production", }, + // Modular imports / tree-shaking for heavy dependencies. Many MUI components + // and icons get pulled in via named imports; without this, the full package + // ships in the initial bundle. Also covers lodash (use lodash-es per import + // path too) and date-fns. + experimental: { + optimizePackageImports: [ + "@mui/material", + "@mui/icons-material", + "@mui/lab", + "@mui/x-data-grid", + "@mui/x-date-pickers", + "date-fns", + "lodash", + "react-icons", + ], + }, + // Rewrites configuration async rewrites() { return [ diff --git a/package-lock.json b/package-lock.json index 068f65bd..4455dc47 100644 --- a/package-lock.json +++ b/package-lock.json @@ -33,7 +33,6 @@ "notistack": "^3.0.2", "qrcode.react": "^4.2.0", "react": "^18.3.1", - "react-beautiful-dnd": "^13.1.1", "react-circular-progressbar": "^2.1.0", "react-confetti": "^6.1.0", "react-cookie": "^8.0.1", @@ -3193,6 +3192,7 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.26.tgz", "integrity": "sha512-RFA/bURkcKzx/X9oumPG9Vp3D3JUgus/d0b67KB0t5S/raciymilkOa66olh78MUI92QLbEJevO7rvqU/kjwKA==", "license": "MIT", + "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.0.2" @@ -3208,27 +3208,6 @@ "@types/react": "^18.0.0" } }, - "node_modules/@types/react-redux": { - "version": "7.1.34", - "resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.34.tgz", - "integrity": "sha512-GdFaVjEbYv4Fthm2ZLvj1VSCedV7TqE5y1kNwnjSdBOTXuRSgowux6J8TAct15T3CKBr63UMk+2CO7ilRhyrAQ==", - "license": "MIT", - "dependencies": { - "@types/hoist-non-react-statics": "^3.3.0", - "@types/react": "*", - "hoist-non-react-statics": "^3.3.0", - "redux": "^4.0.0" - } - }, - "node_modules/@types/react-redux/node_modules/redux": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz", - "integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.9.2" - } - }, "node_modules/@types/react-transition-group": { "version": "4.4.12", "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.12.tgz", @@ -8477,12 +8456,6 @@ "url": "https://opencollective.com/unified" } }, - "node_modules/memoize-one": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz", - "integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==", - "license": "MIT" - }, "node_modules/merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", @@ -10531,66 +10504,6 @@ "node": ">=0.10.0" } }, - "node_modules/react-beautiful-dnd": { - "version": "13.1.1", - "resolved": "https://registry.npmjs.org/react-beautiful-dnd/-/react-beautiful-dnd-13.1.1.tgz", - "integrity": "sha512-0Lvs4tq2VcrEjEgDXHjT98r+63drkKEgqyxdA7qD3mvKwga6a5SscbdLPO2IExotU1jW8L0Ksdl0Cj2AF67nPQ==", - "deprecated": "react-beautiful-dnd is now deprecated. Context and options: https://github.com/atlassian/react-beautiful-dnd/issues/2672", - "license": "Apache-2.0", - "dependencies": { - "@babel/runtime": "^7.9.2", - "css-box-model": "^1.2.0", - "memoize-one": "^5.1.1", - "raf-schd": "^4.0.2", - "react-redux": "^7.2.0", - "redux": "^4.0.4", - "use-memo-one": "^1.1.1" - }, - "peerDependencies": { - "react": "^16.8.5 || ^17.0.0 || ^18.0.0", - "react-dom": "^16.8.5 || ^17.0.0 || ^18.0.0" - } - }, - "node_modules/react-beautiful-dnd/node_modules/react-is": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", - "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", - "license": "MIT" - }, - "node_modules/react-beautiful-dnd/node_modules/react-redux": { - "version": "7.2.9", - "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.9.tgz", - "integrity": "sha512-Gx4L3uM182jEEayZfRbI/G11ZpYdNAnBs70lFVMNdHJI76XYtR+7m0MN+eAs7UHBPhWXcnFPaS+9owSCJQHNpQ==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.15.4", - "@types/react-redux": "^7.1.20", - "hoist-non-react-statics": "^3.3.2", - "loose-envify": "^1.4.0", - "prop-types": "^15.7.2", - "react-is": "^17.0.2" - }, - "peerDependencies": { - "react": "^16.8.3 || ^17 || ^18" - }, - "peerDependenciesMeta": { - "react-dom": { - "optional": true - }, - "react-native": { - "optional": true - } - } - }, - "node_modules/react-beautiful-dnd/node_modules/redux": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz", - "integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.9.2" - } - }, "node_modules/react-circular-progressbar": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/react-circular-progressbar/-/react-circular-progressbar-2.2.0.tgz", @@ -12464,15 +12377,6 @@ } } }, - "node_modules/use-memo-one": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/use-memo-one/-/use-memo-one-1.1.3.tgz", - "integrity": "sha512-g66/K7ZQGYrI6dy8GLpVcMsBp4s17xNkYJVSMvTEevGy3nDxHOfE6z8BVE22+5G5x7t3+bhzrlTDB7ObrEE0cQ==", - "license": "MIT", - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" - } - }, "node_modules/use-sync-external-store": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", diff --git a/package.json b/package.json index 357f2041..ac61cd8d 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,6 @@ "notistack": "^3.0.2", "qrcode.react": "^4.2.0", "react": "^18.3.1", - "react-beautiful-dnd": "^13.1.1", "react-circular-progressbar": "^2.1.0", "react-confetti": "^6.1.0", "react-cookie": "^8.0.1", diff --git a/src/components/LeadForm/LeadForm.js b/src/components/LeadForm/LeadForm.js index 8d94bc23..8240d507 100644 --- a/src/components/LeadForm/LeadForm.js +++ b/src/components/LeadForm/LeadForm.js @@ -13,6 +13,7 @@ import MailOutlineIcon from "@mui/icons-material/MailOutline"; import NotificationsActiveIcon from "@mui/icons-material/NotificationsActive"; import CircularProgress from "@mui/material/CircularProgress"; import { useGoogleReCaptcha } from "react-google-recaptcha-v3"; +import ReCaptchaProvider from "../ReCaptchaProvider"; import { initFacebookPixel, trackEvent, trackForm } from '../../lib/ga'; // Utility functions for bot detection @@ -387,4 +388,13 @@ const LeadForm = () => { ); }; -export default LeadForm; +// Wrap the default export so consumers (homepage, etc.) get the reCAPTCHA +// context without having to mount the provider sitewide. This keeps the +// reCAPTCHA v3 script off of pages that don't render LeadForm. +export default function LeadFormWithRecaptcha() { + return ( + + + + ); +} diff --git a/src/components/ReCaptchaProvider.js b/src/components/ReCaptchaProvider.js new file mode 100644 index 00000000..60026e2b --- /dev/null +++ b/src/components/ReCaptchaProvider.js @@ -0,0 +1,18 @@ +import { GoogleReCaptchaProvider } from "react-google-recaptcha-v3"; + +// Wraps GoogleReCaptchaProvider so pages/components that need reCAPTCHA opt in. +// Previously this lived in _app.js and loaded the reCAPTCHA v3 script on every +// page (homepage, event pages, nonprofit pages, profile, etc.). That's a ~200KB +// download + main-thread cost on pages that don't have any form. +// +// Mount this only around the subtree that contains a form using `useRecaptcha`. +export default function ReCaptchaProvider({ children }) { + return ( + + {children} + + ); +} diff --git a/src/components/TeamCreation/NonprofitRanker.js b/src/components/TeamCreation/NonprofitRanker.js index eefc3a87..8be1b243 100644 --- a/src/components/TeamCreation/NonprofitRanker.js +++ b/src/components/TeamCreation/NonprofitRanker.js @@ -7,7 +7,7 @@ import { Chip, Alert } from '@mui/material'; -import { DragDropContext, Droppable, Draggable } from 'react-beautiful-dnd'; +import { DragDropContext, Droppable, Draggable } from '@hello-pangea/dnd'; import { MdDragHandle } from 'react-icons/md'; /** diff --git a/src/pages/_app.js b/src/pages/_app.js index 75dfa6be..dbaaad3f 100644 --- a/src/pages/_app.js +++ b/src/pages/_app.js @@ -3,7 +3,6 @@ import dynamic from 'next/dynamic' import Head from "next/head"; import CssBaseline from "@mui/material/CssBaseline"; import { AuthProvider } from "@propelauth/react"; -import { GoogleReCaptchaProvider } from "react-google-recaptcha-v3"; import { ThemeProvider } from "@mui/material/styles"; import { Box } from "@mui/material"; import { useRouter } from "next/router"; @@ -71,23 +70,21 @@ export default function MyApp({ Component, pageProps }) { {pageProps.title} - - - - - - - {!isPrintTimelinePage && } - - {!isPrintTimelinePage &&