├── .all-contributorsrc ├── .eslintignore ├── .gitattributes ├── .github └── workflows │ └── validate.yml ├── .gitignore ├── .gitpod.yml ├── .npmrc ├── .prettierignore ├── .prettierrc ├── .vscode ├── extensions.json └── settings.kcd.json ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE.md ├── LIVE_INSTRUCTIONS.md ├── README.md ├── craco.config.js ├── docker-compose.yml ├── jsconfig.json ├── package-lock.json ├── package.json ├── public ├── _redirects ├── favicon.ico ├── index.html ├── manifest.json └── mockServiceWorker.js ├── sandbox.config.json ├── scripts ├── diff.js ├── fix-links ├── pre-commit.js ├── pre-push.js ├── setup.js └── update-deps ├── setup.js └── src ├── __tests__ ├── 01.js ├── 02.js ├── 03.js ├── 04.js ├── 05.js ├── 06.js └── 07.js ├── examples ├── code-splitting │ ├── deps-included │ │ ├── dep.js │ │ └── index.js │ ├── group │ │ ├── one.js │ │ └── two.js │ ├── main.js │ ├── prefetched.js │ └── preloaded.js └── unnecessary-rerenders.js ├── exercise ├── 01.js ├── 01.md ├── 02.js ├── 02.md ├── 03.js ├── 03.md ├── 04.js ├── 04.md ├── 05.js ├── 05.md ├── 06.extra-4.js ├── 06.js ├── 06.md ├── 07.js └── 07.md ├── filter-cities.js ├── final ├── 01.extra-1.js ├── 01.extra-2.js ├── 01.js ├── 02.extra-1.js ├── 02.extra-2.js ├── 02.js ├── 03.extra-1.js ├── 03.extra-2.js ├── 03.js ├── 04.js ├── 05.extra-1.js ├── 05.js ├── 06.extra-1.js ├── 06.extra-2.js ├── 06.extra-3.js ├── 06.extra-4.js ├── 06.js └── 07.js ├── globe ├── countries-110m.json ├── globe.css └── index.js ├── index.js ├── report-profile.js ├── setupTests.js ├── styles.css ├── us-cities.json ├── use-combobox.js ├── utils.js └── workerized-filter-cities.js /.all-contributorsrc: -------------------------------------------------------------------------------- 1 | { 2 | "projectName": "react-performance", 3 | "projectOwner": "kentcdodds", 4 | "repoType": "github", 5 | "files": [ 6 | "README.md" 7 | ], 8 | "imageSize": 100, 9 | "commit": false, 10 | "contributors": [ 11 | { 12 | "login": "kentcdodds", 13 | "name": "Kent C. Dodds", 14 | "avatar_url": "https://avatars.githubusercontent.com/u/1500684?v=3", 15 | "profile": "https://kentcdodds.com", 16 | "contributions": [ 17 | "code", 18 | "doc", 19 | "infra", 20 | "test" 21 | ] 22 | }, 23 | { 24 | "login": "jdorfman", 25 | "name": "Justin Dorfman", 26 | "avatar_url": "https://avatars1.githubusercontent.com/u/398230?v=4", 27 | "profile": "https://stackshare.io/jdorfman/decisions", 28 | "contributions": [ 29 | "fundingFinding" 30 | ] 31 | }, 32 | { 33 | "login": "Fensterbank", 34 | "name": "Frédéric Bolvin", 35 | "avatar_url": "https://avatars2.githubusercontent.com/u/1684826?v=4", 36 | "profile": "https://bol.vin", 37 | "contributions": [ 38 | "doc" 39 | ] 40 | }, 41 | { 42 | "login": "vojtaholik", 43 | "name": "Vojta Holik", 44 | "avatar_url": "https://avatars2.githubusercontent.com/u/25487857?v=4", 45 | "profile": "http://vojta.io", 46 | "contributions": [ 47 | "code", 48 | "design" 49 | ] 50 | }, 51 | { 52 | "login": "marcosvega91", 53 | "name": "Marco Moretti", 54 | "avatar_url": "https://avatars2.githubusercontent.com/u/5365582?v=4", 55 | "profile": "https://github.com/marcosvega91", 56 | "contributions": [ 57 | "code" 58 | ] 59 | }, 60 | { 61 | "login": "rbusquet", 62 | "name": "Ricardo Busquet", 63 | "avatar_url": "https://avatars1.githubusercontent.com/u/7198302?v=4", 64 | "profile": "https://ricardobusquet.com", 65 | "contributions": [ 66 | "code" 67 | ] 68 | }, 69 | { 70 | "login": "nywleswoey", 71 | "name": "Selwyn Yeow", 72 | "avatar_url": "https://avatars3.githubusercontent.com/u/28249994?v=4", 73 | "profile": "https://github.com/nywleswoey", 74 | "contributions": [ 75 | "code" 76 | ] 77 | }, 78 | { 79 | "login": "emzoumpo", 80 | "name": "Emmanouil Zoumpoulakis", 81 | "avatar_url": "https://avatars2.githubusercontent.com/u/2103443?v=4", 82 | "profile": "https://github.com/emzoumpo", 83 | "contributions": [ 84 | "doc" 85 | ] 86 | }, 87 | { 88 | "login": "Aprillion", 89 | "name": "Peter Hozák", 90 | "avatar_url": "https://avatars0.githubusercontent.com/u/1087670?v=4", 91 | "profile": "http://peter.hozak.info/", 92 | "contributions": [ 93 | "code" 94 | ] 95 | }, 96 | { 97 | "login": "PritamSangani", 98 | "name": "Pritam Sangani", 99 | "avatar_url": "https://avatars3.githubusercontent.com/u/22857896?v=4", 100 | "profile": "https://www.linkedin.com/in/pritamsangani/", 101 | "contributions": [ 102 | "code" 103 | ] 104 | }, 105 | { 106 | "login": "milamer", 107 | "name": "Christian Schurr", 108 | "avatar_url": "https://avatars2.githubusercontent.com/u/12884134?v=4", 109 | "profile": "https://github.com/milamer", 110 | "contributions": [ 111 | "code", 112 | "doc" 113 | ] 114 | }, 115 | { 116 | "login": "jmagrippis", 117 | "name": "Johnny Magrippis", 118 | "avatar_url": "https://avatars0.githubusercontent.com/u/3502800?v=4", 119 | "profile": "https://magrippis.com", 120 | "contributions": [ 121 | "code" 122 | ] 123 | }, 124 | { 125 | "login": "AhmedAymanM", 126 | "name": "Ahmed", 127 | "avatar_url": "https://avatars1.githubusercontent.com/u/535126?v=4", 128 | "profile": "https://github.com/AhmedAymanM", 129 | "contributions": [ 130 | "code", 131 | "doc" 132 | ] 133 | }, 134 | { 135 | "login": "RobbertWolfs", 136 | "name": "Robbert Wolfs", 137 | "avatar_url": "https://avatars2.githubusercontent.com/u/12511178?v=4", 138 | "profile": "https://github.com/RobbertWolfs", 139 | "contributions": [ 140 | "doc" 141 | ] 142 | }, 143 | { 144 | "login": "gwanduke", 145 | "name": "Kim Gwan-duk", 146 | "avatar_url": "https://avatars0.githubusercontent.com/u/7443435?v=4", 147 | "profile": "https://www.gwanduke.com", 148 | "contributions": [ 149 | "doc" 150 | ] 151 | }, 152 | { 153 | "login": "rajjejosefsson", 154 | "name": "Rasmus Josefsson", 155 | "avatar_url": "https://avatars2.githubusercontent.com/u/13612444?v=4", 156 | "profile": "https://rasmusjosefsson.com", 157 | "contributions": [ 158 | "code" 159 | ] 160 | }, 161 | { 162 | "login": "MarcosNASA", 163 | "name": "Marcos NASA G", 164 | "avatar_url": "https://avatars3.githubusercontent.com/u/45607262?v=4", 165 | "profile": "https://github.com/MarcosNASA", 166 | "contributions": [ 167 | "doc" 168 | ] 169 | }, 170 | { 171 | "login": "Snaptags", 172 | "name": "Markus Lasermann", 173 | "avatar_url": "https://avatars1.githubusercontent.com/u/1249745?v=4", 174 | "profile": "https://github.com/Snaptags", 175 | "contributions": [ 176 | "doc" 177 | ] 178 | }, 179 | { 180 | "login": "vasilii-kovalev", 181 | "name": "Vasilii Kovalev", 182 | "avatar_url": "https://avatars0.githubusercontent.com/u/10310491?v=4", 183 | "profile": "https://vk.com/vasilii_kovalev", 184 | "contributions": [ 185 | "doc" 186 | ] 187 | }, 188 | { 189 | "login": "matchai", 190 | "name": "Matan Kushner", 191 | "avatar_url": "https://avatars0.githubusercontent.com/u/4658208?v=4", 192 | "profile": "https://github.com/matchai", 193 | "contributions": [ 194 | "doc" 195 | ] 196 | }, 197 | { 198 | "login": "MichaelDeBoey", 199 | "name": "Michaël De Boey", 200 | "avatar_url": "https://avatars3.githubusercontent.com/u/6643991?v=4", 201 | "profile": "https://michaeldeboey.be", 202 | "contributions": [ 203 | "code" 204 | ] 205 | }, 206 | { 207 | "login": "Wekios", 208 | "name": "Veljko Blagojevic", 209 | "avatar_url": "https://avatars2.githubusercontent.com/u/28904821?v=4", 210 | "profile": "http://www.veljkoblagojevic.com", 211 | "contributions": [ 212 | "doc" 213 | ] 214 | }, 215 | { 216 | "login": "bobbywarner", 217 | "name": "Bobby Warner", 218 | "avatar_url": "https://avatars0.githubusercontent.com/u/554961?v=4", 219 | "profile": "http://bobbywarner.com", 220 | "contributions": [ 221 | "code" 222 | ] 223 | }, 224 | { 225 | "login": "Foxandxss", 226 | "name": "Jesús Rodríguez", 227 | "avatar_url": "https://avatars2.githubusercontent.com/u/1087957?v=4", 228 | "profile": "http://angular-tips.com", 229 | "contributions": [ 230 | "doc" 231 | ] 232 | }, 233 | { 234 | "login": "ValentinH", 235 | "name": "Valentin Hervieu", 236 | "avatar_url": "https://avatars2.githubusercontent.com/u/2678610?v=4", 237 | "profile": "https://valentin-hervieu.fr", 238 | "contributions": [ 239 | "bug" 240 | ] 241 | }, 242 | { 243 | "login": "d4vsanchez", 244 | "name": "David Sánchez", 245 | "avatar_url": "https://avatars2.githubusercontent.com/u/2999604?v=4", 246 | "profile": "https://davsanchez.com", 247 | "contributions": [ 248 | "doc" 249 | ] 250 | }, 251 | { 252 | "login": "Merott", 253 | "name": "Merott Movahedi", 254 | "avatar_url": "https://avatars3.githubusercontent.com/u/1757708?v=4", 255 | "profile": "http://merott.com", 256 | "contributions": [ 257 | "doc" 258 | ] 259 | }, 260 | { 261 | "login": "arjenbloemsma", 262 | "name": "Arjen Bloemsma", 263 | "avatar_url": "https://avatars1.githubusercontent.com/u/8061052?v=4", 264 | "profile": "https://www.arjenbloemsma.nl", 265 | "contributions": [ 266 | "doc" 267 | ] 268 | }, 269 | { 270 | "login": "BboyStatix", 271 | "name": "Naveed Elahi", 272 | "avatar_url": "https://avatars.githubusercontent.com/u/19769879?v=4", 273 | "profile": "https://www.linkedin.com/in/syed-naveed-elahi/", 274 | "contributions": [ 275 | "doc" 276 | ] 277 | }, 278 | { 279 | "login": "knappe", 280 | "name": "Tyler Knappe", 281 | "avatar_url": "https://avatars.githubusercontent.com/u/138048?v=4", 282 | "profile": "http://tknappe.com/", 283 | "contributions": [ 284 | "doc" 285 | ] 286 | }, 287 | { 288 | "login": "0xnoob", 289 | "name": "0xnoob", 290 | "avatar_url": "https://avatars.githubusercontent.com/u/49793844?v=4", 291 | "profile": "https://github.com/0xnoob", 292 | "contributions": [ 293 | "code", 294 | "doc" 295 | ] 296 | }, 297 | { 298 | "login": "esplito", 299 | "name": "Emil Esplund", 300 | "avatar_url": "https://avatars.githubusercontent.com/u/10499067?v=4", 301 | "profile": "https://emildev.netlify.app/", 302 | "contributions": [ 303 | "test" 304 | ] 305 | }, 306 | { 307 | "login": "jstheoriginal", 308 | "name": "Justin Stanley", 309 | "avatar_url": "https://avatars.githubusercontent.com/u/5117473?v=4", 310 | "profile": "http://swiftwithjustin.co", 311 | "contributions": [ 312 | "test" 313 | ] 314 | }, 315 | { 316 | "login": "pvinis", 317 | "name": "Pavlos Vinieratos", 318 | "avatar_url": "https://avatars.githubusercontent.com/u/100233?v=4", 319 | "profile": "http://pavlos.dev", 320 | "contributions": [ 321 | "doc" 322 | ] 323 | }, 324 | { 325 | "login": "Reignable", 326 | "name": "Joe Barrett", 327 | "avatar_url": "https://avatars.githubusercontent.com/u/18505669?v=4", 328 | "profile": "https://github.com/Reignable", 329 | "contributions": [ 330 | "code" 331 | ] 332 | }, 333 | { 334 | "login": "LochMess", 335 | "name": "LochMess", 336 | "avatar_url": "https://avatars.githubusercontent.com/u/18762221?v=4", 337 | "profile": "https://github.com/LochMess", 338 | "contributions": [ 339 | "doc" 340 | ] 341 | }, 342 | { 343 | "login": "mstosio", 344 | "name": "Michal", 345 | "avatar_url": "https://avatars.githubusercontent.com/u/10117225?v=4", 346 | "profile": "http://codewars.com/users/mstosio", 347 | "contributions": [ 348 | "doc" 349 | ] 350 | }, 351 | { 352 | "login": "marioleed", 353 | "name": "Mario Sannum", 354 | "avatar_url": "https://avatars.githubusercontent.com/u/1763448?v=4", 355 | "profile": "https://github.com/marioleed", 356 | "contributions": [ 357 | "code" 358 | ] 359 | }, 360 | { 361 | "login": "CJThornburg", 362 | "name": "CJThornburg", 363 | "avatar_url": "https://avatars.githubusercontent.com/u/59716885?v=4", 364 | "profile": "https://github.com/CJThornburg", 365 | "contributions": [ 366 | "doc" 367 | ] 368 | }, 369 | { 370 | "login": "lucianoayres", 371 | "name": "Luciano Ayres", 372 | "avatar_url": "https://avatars.githubusercontent.com/u/20209393?v=4", 373 | "profile": "http://www.lucianoayres.com.br", 374 | "contributions": [ 375 | "doc" 376 | ] 377 | }, 378 | { 379 | "login": "skgyan", 380 | "name": "Sushil Kumar", 381 | "avatar_url": "https://avatars.githubusercontent.com/u/5046860?v=4", 382 | "profile": "https://github.com/skgyan", 383 | "contributions": [ 384 | "code" 385 | ] 386 | }, 387 | { 388 | "login": "NishuGoel", 389 | "name": "Nishu Goel", 390 | "avatar_url": "https://avatars.githubusercontent.com/u/26349046?v=4", 391 | "profile": "http://unravelweb.dev", 392 | "contributions": [ 393 | "doc" 394 | ] 395 | }, 396 | { 397 | "login": "creador-dev", 398 | "name": "Pawan Kumar", 399 | "avatar_url": "https://avatars.githubusercontent.com/u/40248406?v=4", 400 | "profile": "https://creador.dev", 401 | "contributions": [ 402 | "doc" 403 | ] 404 | }, 405 | { 406 | "login": "xhocopuff", 407 | "name": "Melesia Thompson", 408 | "avatar_url": "https://avatars.githubusercontent.com/u/62633485?v=4", 409 | "profile": "https://github.com/xhocopuff", 410 | "contributions": [ 411 | "doc" 412 | ] 413 | } 414 | ], 415 | "repoHost": "https://github.com", 416 | "contributorsPerLine": 7, 417 | "skipCi": true, 418 | "commitType": "docs", 419 | "commitConvention": "angular" 420 | } 421 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | build 4 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | -------------------------------------------------------------------------------- /.github/workflows/validate.yml: -------------------------------------------------------------------------------- 1 | name: validate 2 | on: 3 | push: 4 | branches: 5 | - 'main' 6 | pull_request: 7 | branches: 8 | - 'main' 9 | jobs: 10 | setup: 11 | # ignore all-contributors PRs 12 | if: ${{ !contains(github.head_ref, 'all-contributors') }} 13 | strategy: 14 | matrix: 15 | os: [ubuntu-latest, windows-latest, macos-latest] 16 | runs-on: ${{ matrix.os }} 17 | steps: 18 | - name: ⬇️ Checkout repo 19 | uses: actions/checkout@v3 20 | 21 | - name: ⎔ Setup node 22 | uses: actions/setup-node@v3 23 | with: 24 | node-version: 18 25 | 26 | - name: npm 8 27 | run: npm install --global npm@8 28 | 29 | - name: ▶️ Run setup script 30 | run: npm run setup 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | coverage 4 | build 5 | .idea/ 6 | .vscode/ 7 | .eslintcache 8 | -------------------------------------------------------------------------------- /.gitpod.yml: -------------------------------------------------------------------------------- 1 | ## Learn more about this file at 'https://www.gitpod.io/docs/references/gitpod-yml' 2 | ## 3 | ## This '.gitpod.yml' file when placed at the root of a project instructs 4 | ## Gitpod how to prepare & build the project, start development environments 5 | ## and configure continuous prebuilds. Prebuilds when enabled builds a project 6 | ## like a CI server so you can start coding right away - no more waiting for 7 | ## dependencies to download and builds to finish when reviewing pull-requests 8 | ## or hacking on something new. 9 | ## 10 | ## With Gitpod you can develop software from any device (even iPads) via 11 | ## desktop or browser based versions of VS Code or any JetBrains IDE and 12 | ## customise it to your individual needs - from themes to extensions, you 13 | ## have full control. 14 | ## 15 | ## The easiest way to try out Gitpod is install the browser extenion: 16 | ## 'https://www.gitpod.io/docs/browser-extension' or by prefixing 17 | ## 'https://gitpod.io#' to the source control URL of any project. 18 | ## 19 | ## For example: 'https://gitpod.io#https://github.com/gitpod-io/gitpod' 20 | 21 | 22 | ## The 'tasks' section defines how Gitpod prepares & builds this project 23 | ## and how it can start development servers. With Gitpod, there are three 24 | ## types of tasks: 25 | ## 26 | ## - before: Use this for tasks that need to run before init and before command. 27 | ## - init: Use this to configure prebuilds of heavy-lifting tasks such as 28 | ## downloading dependencies or compiling source code. 29 | ## - command: Use this to start your database or application when the workspace starts. 30 | ## 31 | ## Learn more about these tasks at 'https://www.gitpod.io/docs/config-start-tasks' 32 | 33 | tasks: 34 | - name: App 35 | init: npm install 36 | command: npm run start 37 | openMode: split-left 38 | 39 | - name: Test 40 | command: npm run test 41 | openMode: split-right 42 | 43 | - name: Set up email 44 | command: | 45 | clear 46 | printf "\n\n\n" 47 | printf "\u001b[36;1mAutofilling Email\u001b[0m\n" 48 | printf "\u001b[2;1mEach exercise comes with a elaboration form to help your retention. Providing your email now will mean you don't have to provide it each time you fill out the form.\u001b[0m\n" 49 | npx "https://gist.github.com/kentcdodds/2d44448a8997b9964b1be44cd294d1f5" \ 50 | && exit 0 51 | ## The 'ports' section defines various ports your may listen on are 52 | ## configured in Gitpod on an authenticated URL. By default, all ports 53 | ## are in private visibility state. 54 | ## 55 | ## Learn more about ports at 'https://www.gitpod.io/docs/config-ports' 56 | 57 | ports: 58 | - port: 3000 # alternatively configure entire ranges via '8080-8090' 59 | visibility: private # either 'public' or 'private' (default) 60 | onOpen: open-browser # either 'open-browser', 'open-preview' or 'ignore' 61 | 62 | ## The 'vscode' section defines a list of Visual Studio Code extensions from 63 | ## the OpenVSX.org registry to be installed upon workspace startup. OpenVSX 64 | ## is an open alternative to the proprietary Visual Studio Code Marketplace 65 | ## and extensions can be added by sending a pull-request with the extension 66 | ## identifier to https://github.com/open-vsx/publish-extensions 67 | ## 68 | ## The identifier of an extension is always ${publisher}.${name}. 69 | ## 70 | ## For example: 'vscodevim.vim' 71 | ## 72 | ## Learn more at 'https://www.gitpod.io/docs/ides-and-editors/vscode' 73 | 74 | vscode: 75 | extensions: 76 | - VisualStudioExptTeam.vscodeintellicode 77 | - dbaeumer.vscode-eslint 78 | - formulahendry.auto-rename-tag 79 | - esbenp.prettier-vscode 80 | - ms-azuretools.vscode-docker 81 | 82 | ## The 'github' section defines configuration of continuous prebuilds 83 | ## for GitHub repositories when the GitHub application 84 | ## 'https://github.com/apps/gitpod-io' is installed in GitHub and granted 85 | ## permissions to access the repository. 86 | ## 87 | ## Learn more at 'https://www.gitpod.io/docs/prebuilds' 88 | 89 | github: 90 | prebuilds: 91 | # enable for the default branch 92 | master: true 93 | # enable for all branches in this repo 94 | branches: false 95 | # enable for pull requests coming from this repo 96 | pullRequests: false 97 | # enable for pull requests coming from forks 98 | pullRequestsFromForks: false 99 | # add a check to pull requests 100 | addCheck: false 101 | # add a "Review in Gitpod" button as a comment to pull requests 102 | addComment: false 103 | # add a "Review in Gitpod" button to the pull request's description 104 | addBadge: false 105 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | registry=https://registry.npmjs.org/ 2 | package-lock=true 3 | yes=true 4 | legacy-peer-deps=true 5 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | build 4 | src/globe/countries-110m.json 5 | src/us-cities.json 6 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "avoid", 3 | "bracketSpacing": false, 4 | "endOfLine": "lf", 5 | "htmlWhitespaceSensitivity": "css", 6 | "insertPragma": false, 7 | "jsxBracketSameLine": false, 8 | "jsxSingleQuote": false, 9 | "printWidth": 80, 10 | "proseWrap": "always", 11 | "quoteProps": "as-needed", 12 | "requirePragma": false, 13 | "semi": false, 14 | "singleQuote": true, 15 | "tabWidth": 2, 16 | "trailingComma": "all", 17 | "useTabs": false 18 | } 19 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "dbaeumer.vscode-eslint", 4 | "esbenp.prettier-vscode", 5 | "formulahendry.auto-rename-tag", 6 | "VisualStudioExptTeam.vscodeintellicode" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /.vscode/settings.kcd.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.defaultFormatter": "esbenp.prettier-vscode", 3 | "editor.formatOnSave": true, 4 | "editor.detectIndentation": true, 5 | "editor.fontFamily": "'Dank Mono', Menlo, Monaco, 'Courier New', monospace", 6 | "editor.fontLigatures": false, 7 | "editor.rulers": [80], 8 | "editor.snippetSuggestions": "top", 9 | "editor.wordBasedSuggestions": false, 10 | "editor.suggest.localityBonus": true, 11 | "editor.acceptSuggestionOnCommitCharacter": false, 12 | "[javascript]": { 13 | "editor.defaultFormatter": "esbenp.prettier-vscode", 14 | "editor.suggestSelection": "recentlyUsed", 15 | "editor.suggest.showKeywords": false 16 | }, 17 | "editor.renderWhitespace": "boundary", 18 | "files.defaultLanguage": "{activeEditorLanguage}", 19 | "javascript.validate.enable": false, 20 | "search.exclude": { 21 | "**/node_modules": true, 22 | "**/bower_components": true, 23 | "**/coverage": true, 24 | "**/dist": true, 25 | "**/build": true, 26 | "**/.build": true, 27 | "**/.gh-pages": true 28 | }, 29 | "editor.codeActionsOnSave": { 30 | "source.fixAll.eslint": false 31 | }, 32 | "eslint.validate": [ 33 | "javascript", 34 | "javascriptreact", 35 | "typescript", 36 | "typescriptreact" 37 | ], 38 | "eslint.options": { 39 | "env": { 40 | "browser": true, 41 | "jest/globals": true, 42 | "es6": true 43 | }, 44 | "parserOptions": { 45 | "ecmaVersion": 2019, 46 | "sourceType": "module", 47 | "ecmaFeatures": { 48 | "jsx": true 49 | } 50 | }, 51 | "rules": { 52 | "no-debugger": "off" 53 | } 54 | }, 55 | "workbench.colorTheme": "Night Owl", 56 | "workbench.iconTheme": "material-icon-theme", 57 | "breadcrumbs.enabled": true, 58 | "grunt.autoDetect": "off", 59 | "gulp.autoDetect": "off", 60 | "npm.runSilent": true, 61 | "explorer.confirmDragAndDrop": false, 62 | "editor.formatOnPaste": false, 63 | "editor.cursorSmoothCaretAnimation": true, 64 | "editor.smoothScrolling": true, 65 | "php.suggest.basic": false 66 | } 67 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | Please refer to [kentcdodds.com/conduct/](https://kentcdodds.com/conduct/) 2 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Thanks for being willing to contribute! 4 | 5 | **Working on your first Pull Request?** You can learn how from this _free_ 6 | series [How to Contribute to an Open Source Project on GitHub][egghead] 7 | 8 | ## Project setup 9 | 10 | 1. Fork and clone the repo 11 | 2. Run `npm run setup -s` to install dependencies and run validation 12 | 3. Create a branch for your PR with `git checkout -b pr/your-branch-name` 13 | 14 | > Tip: Keep your `main` branch pointing at the original repository and make 15 | > pull requests from branches on your fork. To do this, run: 16 | > 17 | > ``` 18 | > git remote add upstream https://github.com/kentcdodds/react-performance.git 19 | > git fetch upstream 20 | > git branch --set-upstream-to=upstream/main main 21 | > ``` 22 | > 23 | > This will add the original repository as a "remote" called "upstream," Then 24 | > fetch the git information from that remote, then set your local `main` 25 | > branch to use the upstream main branch whenever you run `git pull`. Then you 26 | > can make all of your pull request branches based on this `main` branch. 27 | > Whenever you want to update your version of `main`, do a regular `git pull`. 28 | 29 | ## Help needed 30 | 31 | Please checkout the [the open issues][issues] 32 | 33 | Also, please watch the repo and respond to questions/bug reports/feature 34 | requests! Thanks! 35 | 36 | [egghead]: 37 | https://egghead.io/series/how-to-contribute-to-an-open-source-project-on-github 38 | [issues]: https://github.com/kentcdodds/react-performance/issues 39 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:16 2 | 3 | WORKDIR /app 4 | COPY . . 5 | RUN NO_EMAIL_AUTOFILL=true node setup 6 | 7 | CMD ["npm", "start"] 8 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | This material is available for private, non-commercial use under the 2 | [GPL version 3](http://www.gnu.org/licenses/gpl-3.0-standalone.html). If you 3 | would like to use this material to conduct your own workshop, please contact me 4 | at me@kentcdodds.com 5 | -------------------------------------------------------------------------------- /LIVE_INSTRUCTIONS.md: -------------------------------------------------------------------------------- 1 | # Live Workshop Instructions 2 | 3 | Hey there 👋 I'm Kent C. Dodds. Here's some info about me: 4 | 5 | - 🏡 Utah 6 | - 👩 👧 👦 👦 👦 🐕 7 | - 🏢 https://kentcdodds.com 8 | - 🐦/🐙 @kentcdodds 9 | - 🏆 https://TestingJavaScript.com 10 | - 👩‍🚀 https://EpicReact.Dev 11 | - 💬 https://kcd.im/discord 12 | - ❓ https://kcd.im/office-hours 13 | - 💻 https://kcd.im/workshops 14 | - 🎙 https://kcd.im/podcast 15 | - 💌 https://kcd.im/news 16 | - 📝 https://kcd.im/blog 17 | - 📺 https://kcd.im/devtips 18 | - 👨‍💻 https://kcd.im/coding 19 | - 📽 https://kcd.im/youtube 20 | 21 | This workshop is part of the series of self-paced workshops on 22 | [EpicReact.Dev](https://epicreact.dev). This document explains a few things 23 | you'll need to know if you're attending a live version of this workshop. 24 | 25 | ## Pre-Workshop Instructions/Requirements 26 | 27 | Please watch the 28 | [EpicReact.Dev Welcome Videos](https://epicreact.dev/modules/welcome-to-epic-react) 29 | (~30 minutes). If you follow along with this repo, you should be all set up by 30 | the end of it and you'll be ready to go. 31 | 32 | **NOTE: I will assume you know how to work through the exercises.** There will 33 | be _no time_ given for troubleshooting setup issues or answering questions about 34 | exercise logistics. 35 | 36 | Here are the basic things you need to make sure you do (in addition to watching 37 | [the example run through](https://epicreact.dev/modules/welcome-to-epic-react/example-runthrough)): 38 | 39 | - [ ] Ensure you satisfy all the "Prerequisites" and "System Requirements" found 40 | in the `README.md`. 41 | - [ ] Run the project setup as documented in the `README.md` (~5 minutes) 42 | 43 | If our workshop is remote via Zoom: 44 | 45 | - [ ] Install and setup [Zoom](https://zoom.us) on the computer you will be 46 | using (~5 minutes) 47 | - [ ] If our Watch 48 | [Use Zoom for KCD Workshops](https://egghead.io/lessons/egghead-use-zoom-for-kcd-workshops) 49 | (~8 minutes). 50 | 51 | ### Schedule 52 | 53 | Here's the general schedule for the workshop (this is flexible): 54 | 55 | - 😴 Logistics 56 | - 💪 1. Code splitting 57 | - 😴 10 Minutes 58 | - 💪 2. useMemo for expensive calculations 59 | - 💪 3. React.memo for reducing unnecessary re-renders 60 | - 🌮 30 Minutes 61 | - 💪 4. Window large lists with react-virtual 62 | - 😴 10 Minutes 63 | - 💪 5. Optimize context value 64 | - 💪 6. Fix "perf death by a thousand cuts" 65 | - 😴 10 Minutes 66 | - 💪 7. Production performance monitoring 67 | - ❓ Q&A 68 | 69 | ### Questions 70 | 71 | Please do ask! **Interrupt me.** If you have an unrelated question, please save 72 | them for [my office hours](https://kcd.im/office-hours). 73 | 74 | ### For remote workshops: 75 | 76 | - Help us make this more human by keeping your video on if possible 77 | - Keep microphone muted unless speaking 78 | - Make the most of breakout rooms during exercises 79 | -------------------------------------------------------------------------------- /craco.config.js: -------------------------------------------------------------------------------- 1 | module.exports = require('@kentcdodds/react-workshop-app/craco.config') 2 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | node: 5 | build: . 6 | volumes: 7 | - ./src:/app/src 8 | ports: 9 | - '3000:3000' 10 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "./src" 4 | }, 5 | "include": ["src"] 6 | } 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-performance", 3 | "author": "Kent C. Dodds (https://kentcdodds.com/)", 4 | "title": "React Performance ⚡", 5 | "private": true, 6 | "version": "1.0.0", 7 | "description": "React Performance Workshop", 8 | "keywords": [], 9 | "homepage": "https://react-performance.netlify.app/", 10 | "license": "GPL-3.0-only", 11 | "main": "src/index.js", 12 | "engines": { 13 | "node": ">=16", 14 | "npm": ">=8.16.0" 15 | }, 16 | "dependencies": { 17 | "@jackfranklin/test-data-bot": "^1.3.0", 18 | "@kentcdodds/react-workshop-app": "^6.0.1", 19 | "@testing-library/react": "^13.3.0", 20 | "@testing-library/user-event": "^14.2.1", 21 | "chalk": "^4.1.2", 22 | "codegen.macro": "^4.1.0", 23 | "d3-geo": "^2.0.1", 24 | "downshift": "^6.1.7", 25 | "history": "^5.3.0", 26 | "lodash": "^4.17.21", 27 | "match-sorter": "^6.3.1", 28 | "react": "^18.2.0", 29 | "react-dom": "^18.2.0", 30 | "react-spring": "^9.4.5", 31 | "react-use-gesture": "^9.1.3", 32 | "react-virtual": "^2.10.4", 33 | "recoil": "0.1.2", 34 | "topojson-client": "^3.1.0", 35 | "use-interval": "^1.4.0", 36 | "vanilla-tilt": "^1.7.2", 37 | "workerize": "^0.1.8", 38 | "workerize-loader": "^2.0.2" 39 | }, 40 | "devDependencies": { 41 | "@craco/craco": "^6.4.3", 42 | "@types/react": "^18.0.14", 43 | "@types/react-dom": "^18.0.5", 44 | "husky": "^4.3.8", 45 | "npm-run-all": "^4.1.5", 46 | "prettier": "^2.7.1", 47 | "react-scripts": "^5.0.1", 48 | "serve": "^13.0.2" 49 | }, 50 | "scripts": { 51 | "start": "craco start", 52 | "build": "craco build", 53 | "build:profile": "craco build --profile", 54 | "serve": "serve -s build", 55 | "test": "craco test", 56 | "test:coverage": "npm run test -- --watchAll=false --coverage", 57 | "test:exercises": "npm run test -- testing.*exercises\\/ --onlyChanged", 58 | "setup": "node setup", 59 | "lint": "eslint .", 60 | "format": "prettier --write \"./src\"", 61 | "validate": "npm-run-all --parallel build:profile test:coverage lint" 62 | }, 63 | "husky": { 64 | "hooks": { 65 | "pre-commit": "node ./scripts/pre-commit", 66 | "pre-push": "node ./scripts/pre-push" 67 | } 68 | }, 69 | "jest": { 70 | "collectCoverageFrom": [ 71 | "src/final/**/*.js" 72 | ] 73 | }, 74 | "eslintConfig": { 75 | "extends": "react-app" 76 | }, 77 | "browserslist": { 78 | "development": [ 79 | "last 2 chrome versions", 80 | "last 2 firefox versions", 81 | "last 2 edge versions" 82 | ], 83 | "production": [ 84 | ">1%", 85 | "last 4 versions", 86 | "Firefox ESR", 87 | "not ie < 11" 88 | ] 89 | }, 90 | "repository": { 91 | "type": "git", 92 | "url": "git+https://github.com/kentcdodds/react-performance.git" 93 | }, 94 | "bugs": { 95 | "url": "https://github.com/kentcdodds/react-performance/issues" 96 | }, 97 | "msw": { 98 | "workerDirectory": "public" 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /public/_redirects: -------------------------------------------------------------------------------- 1 | /* /index.html 200 -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kentcdodds/react-performance/f636f1b9018f3001d83c215fcb7cb12928de2a40/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 11 | 12 | React Performance ⚡ 13 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React Performance", 3 | "name": "React Performance", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#1675ff", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /public/mockServiceWorker.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Mock Service Worker. 3 | * @see https://github.com/mswjs/msw 4 | * - Please do NOT modify this file. 5 | * - Please do NOT serve this file on production. 6 | */ 7 | /* eslint-disable */ 8 | /* tslint:disable */ 9 | 10 | const INTEGRITY_CHECKSUM = '82ef9b96d8393b6da34527d1d6e19187' 11 | const bypassHeaderName = 'x-msw-bypass' 12 | const activeClientIds = new Set() 13 | 14 | self.addEventListener('install', function () { 15 | return self.skipWaiting() 16 | }) 17 | 18 | self.addEventListener('activate', async function (event) { 19 | return self.clients.claim() 20 | }) 21 | 22 | self.addEventListener('message', async function (event) { 23 | const clientId = event.source.id 24 | 25 | if (!clientId || !self.clients) { 26 | return 27 | } 28 | 29 | const client = await self.clients.get(clientId) 30 | 31 | if (!client) { 32 | return 33 | } 34 | 35 | const allClients = await self.clients.matchAll() 36 | 37 | switch (event.data) { 38 | case 'KEEPALIVE_REQUEST': { 39 | sendToClient(client, { 40 | type: 'KEEPALIVE_RESPONSE', 41 | }) 42 | break 43 | } 44 | 45 | case 'INTEGRITY_CHECK_REQUEST': { 46 | sendToClient(client, { 47 | type: 'INTEGRITY_CHECK_RESPONSE', 48 | payload: INTEGRITY_CHECKSUM, 49 | }) 50 | break 51 | } 52 | 53 | case 'MOCK_ACTIVATE': { 54 | activeClientIds.add(clientId) 55 | 56 | sendToClient(client, { 57 | type: 'MOCKING_ENABLED', 58 | payload: true, 59 | }) 60 | break 61 | } 62 | 63 | case 'MOCK_DEACTIVATE': { 64 | activeClientIds.delete(clientId) 65 | break 66 | } 67 | 68 | case 'CLIENT_CLOSED': { 69 | activeClientIds.delete(clientId) 70 | 71 | const remainingClients = allClients.filter((client) => { 72 | return client.id !== clientId 73 | }) 74 | 75 | // Unregister itself when there are no more clients 76 | if (remainingClients.length === 0) { 77 | self.registration.unregister() 78 | } 79 | 80 | break 81 | } 82 | } 83 | }) 84 | 85 | // Resolve the "master" client for the given event. 86 | // Client that issues a request doesn't necessarily equal the client 87 | // that registered the worker. It's with the latter the worker should 88 | // communicate with during the response resolving phase. 89 | async function resolveMasterClient(event) { 90 | const client = await self.clients.get(event.clientId) 91 | 92 | if (client.frameType === 'top-level') { 93 | return client 94 | } 95 | 96 | const allClients = await self.clients.matchAll() 97 | 98 | return allClients 99 | .filter((client) => { 100 | // Get only those clients that are currently visible. 101 | return client.visibilityState === 'visible' 102 | }) 103 | .find((client) => { 104 | // Find the client ID that's recorded in the 105 | // set of clients that have registered the worker. 106 | return activeClientIds.has(client.id) 107 | }) 108 | } 109 | 110 | async function handleRequest(event, requestId) { 111 | const client = await resolveMasterClient(event) 112 | const response = await getResponse(event, client, requestId) 113 | 114 | // Send back the response clone for the "response:*" life-cycle events. 115 | // Ensure MSW is active and ready to handle the message, otherwise 116 | // this message will pend indefinitely. 117 | if (client && activeClientIds.has(client.id)) { 118 | ;(async function () { 119 | const clonedResponse = response.clone() 120 | sendToClient(client, { 121 | type: 'RESPONSE', 122 | payload: { 123 | requestId, 124 | type: clonedResponse.type, 125 | ok: clonedResponse.ok, 126 | status: clonedResponse.status, 127 | statusText: clonedResponse.statusText, 128 | body: 129 | clonedResponse.body === null ? null : await clonedResponse.text(), 130 | headers: serializeHeaders(clonedResponse.headers), 131 | redirected: clonedResponse.redirected, 132 | }, 133 | }) 134 | })() 135 | } 136 | 137 | return response 138 | } 139 | 140 | async function getResponse(event, client, requestId) { 141 | const { request } = event 142 | const requestClone = request.clone() 143 | const getOriginalResponse = () => fetch(requestClone) 144 | 145 | // Bypass mocking when the request client is not active. 146 | if (!client) { 147 | return getOriginalResponse() 148 | } 149 | 150 | // Bypass initial page load requests (i.e. static assets). 151 | // The absence of the immediate/parent client in the map of the active clients 152 | // means that MSW hasn't dispatched the "MOCK_ACTIVATE" event yet 153 | // and is not ready to handle requests. 154 | if (!activeClientIds.has(client.id)) { 155 | return await getOriginalResponse() 156 | } 157 | 158 | // Bypass requests with the explicit bypass header 159 | if (requestClone.headers.get(bypassHeaderName) === 'true') { 160 | const cleanRequestHeaders = serializeHeaders(requestClone.headers) 161 | 162 | // Remove the bypass header to comply with the CORS preflight check. 163 | delete cleanRequestHeaders[bypassHeaderName] 164 | 165 | const originalRequest = new Request(requestClone, { 166 | headers: new Headers(cleanRequestHeaders), 167 | }) 168 | 169 | return fetch(originalRequest) 170 | } 171 | 172 | // Send the request to the client-side MSW. 173 | const reqHeaders = serializeHeaders(request.headers) 174 | const body = await request.text() 175 | 176 | const clientMessage = await sendToClient(client, { 177 | type: 'REQUEST', 178 | payload: { 179 | id: requestId, 180 | url: request.url, 181 | method: request.method, 182 | headers: reqHeaders, 183 | cache: request.cache, 184 | mode: request.mode, 185 | credentials: request.credentials, 186 | destination: request.destination, 187 | integrity: request.integrity, 188 | redirect: request.redirect, 189 | referrer: request.referrer, 190 | referrerPolicy: request.referrerPolicy, 191 | body, 192 | bodyUsed: request.bodyUsed, 193 | keepalive: request.keepalive, 194 | }, 195 | }) 196 | 197 | switch (clientMessage.type) { 198 | case 'MOCK_SUCCESS': { 199 | return delayPromise( 200 | () => respondWithMock(clientMessage), 201 | clientMessage.payload.delay, 202 | ) 203 | } 204 | 205 | case 'MOCK_NOT_FOUND': { 206 | return getOriginalResponse() 207 | } 208 | 209 | case 'NETWORK_ERROR': { 210 | const { name, message } = clientMessage.payload 211 | const networkError = new Error(message) 212 | networkError.name = name 213 | 214 | // Rejecting a request Promise emulates a network error. 215 | throw networkError 216 | } 217 | 218 | case 'INTERNAL_ERROR': { 219 | const parsedBody = JSON.parse(clientMessage.payload.body) 220 | 221 | console.error( 222 | `\ 223 | [MSW] Request handler function for "%s %s" has thrown the following exception: 224 | 225 | ${parsedBody.errorType}: ${parsedBody.message} 226 | (see more detailed error stack trace in the mocked response body) 227 | 228 | This exception has been gracefully handled as a 500 response, however, it's strongly recommended to resolve this error. 229 | If you wish to mock an error response, please refer to this guide: https://mswjs.io/docs/recipes/mocking-error-responses\ 230 | `, 231 | request.method, 232 | request.url, 233 | ) 234 | 235 | return respondWithMock(clientMessage) 236 | } 237 | } 238 | 239 | return getOriginalResponse() 240 | } 241 | 242 | self.addEventListener('fetch', function (event) { 243 | const { request } = event 244 | 245 | // Bypass navigation requests. 246 | if (request.mode === 'navigate') { 247 | return 248 | } 249 | 250 | // Opening the DevTools triggers the "only-if-cached" request 251 | // that cannot be handled by the worker. Bypass such requests. 252 | if (request.cache === 'only-if-cached' && request.mode !== 'same-origin') { 253 | return 254 | } 255 | 256 | // Bypass all requests when there are no active clients. 257 | // Prevents the self-unregistered worked from handling requests 258 | // after it's been deleted (still remains active until the next reload). 259 | if (activeClientIds.size === 0) { 260 | return 261 | } 262 | 263 | const requestId = uuidv4() 264 | 265 | return event.respondWith( 266 | handleRequest(event, requestId).catch((error) => { 267 | console.error( 268 | '[MSW] Failed to mock a "%s" request to "%s": %s', 269 | request.method, 270 | request.url, 271 | error, 272 | ) 273 | }), 274 | ) 275 | }) 276 | 277 | function serializeHeaders(headers) { 278 | const reqHeaders = {} 279 | headers.forEach((value, name) => { 280 | reqHeaders[name] = reqHeaders[name] 281 | ? [].concat(reqHeaders[name]).concat(value) 282 | : value 283 | }) 284 | return reqHeaders 285 | } 286 | 287 | function sendToClient(client, message) { 288 | return new Promise((resolve, reject) => { 289 | const channel = new MessageChannel() 290 | 291 | channel.port1.onmessage = (event) => { 292 | if (event.data && event.data.error) { 293 | return reject(event.data.error) 294 | } 295 | 296 | resolve(event.data) 297 | } 298 | 299 | client.postMessage(JSON.stringify(message), [channel.port2]) 300 | }) 301 | } 302 | 303 | function delayPromise(cb, duration) { 304 | return new Promise((resolve) => { 305 | setTimeout(() => resolve(cb()), duration) 306 | }) 307 | } 308 | 309 | function respondWithMock(clientMessage) { 310 | return new Response(clientMessage.payload.body, { 311 | ...clientMessage.payload, 312 | headers: clientMessage.payload.headers, 313 | }) 314 | } 315 | 316 | function uuidv4() { 317 | return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { 318 | const r = (Math.random() * 16) | 0 319 | const v = c == 'x' ? r : (r & 0x3) | 0x8 320 | return v.toString(16) 321 | }) 322 | } 323 | -------------------------------------------------------------------------------- /sandbox.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "template": "node", 3 | "container": { 4 | "startScript": "start", 5 | "port": 3000, 6 | "node": "14" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /scripts/diff.js: -------------------------------------------------------------------------------- 1 | const {spawnSync} = require('child_process') 2 | const inquirer = require('inquirer') 3 | const glob = require('glob') 4 | 5 | async function go() { 6 | const files = glob 7 | .sync('src/+(exercise|final)/*.+(js|ts|tsx)', { 8 | ignore: ['*.d.ts'], 9 | }) 10 | .map(f => f.replace(/^src\//, '')) 11 | const {first} = await inquirer.prompt([ 12 | { 13 | name: 'first', 14 | message: `What's the first file`, 15 | type: 'list', 16 | choices: files, 17 | }, 18 | ]) 19 | const {second} = await inquirer.prompt([ 20 | { 21 | name: 'second', 22 | message: `What's the second file`, 23 | type: 'list', 24 | choices: files.filter(f => f !== first), 25 | }, 26 | ]) 27 | 28 | spawnSync(`git diff --no-index ./src/${first} ./src/${second}`, { 29 | shell: true, 30 | stdio: 'inherit', 31 | }) 32 | } 33 | 34 | go() 35 | -------------------------------------------------------------------------------- /scripts/fix-links: -------------------------------------------------------------------------------- 1 | npx https://gist.github.com/kentcdodds/436a77ff8977269e5fee39d9d89956de 2 | npm run format 3 | -------------------------------------------------------------------------------- /scripts/pre-commit.js: -------------------------------------------------------------------------------- 1 | var spawnSync = require('child_process').spawnSync 2 | const {username} = require('os').userInfo() 3 | 4 | if (username === 'kentcdodds') { 5 | const result = spawnSync('npm run validate', {stdio: 'inherit', shell: true}) 6 | 7 | if (result.status !== 0) { 8 | process.exit(result.status) 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /scripts/pre-push.js: -------------------------------------------------------------------------------- 1 | try { 2 | const {username} = require('os').userInfo() 3 | const { 4 | repository: {url: repoUrl}, 5 | } = require('../package.json') 6 | 7 | const remote = process.env.HUSKY_GIT_PARAMS.split(' ')[1] 8 | const repoName = repoUrl.match(/(?:.(?!\/))+\.git$/)[0] 9 | if (username !== 'kentcdodds' && remote.includes(`kentcdodds${repoName}`)) { 10 | console.log( 11 | `You're trying to push to Kent's repo directly. If you want to save and push your work or even make a contribution to the workshop material, you'll need to fork the repo first and push changes to your fork. Learn how here: https://docs.github.com/en/free-pro-team@latest/github/getting-started-with-github/fork-a-repo`, 12 | ) 13 | process.exit(1) 14 | } 15 | } catch (error) { 16 | // ignore 17 | } 18 | -------------------------------------------------------------------------------- /scripts/setup.js: -------------------------------------------------------------------------------- 1 | var spawnSync = require('child_process').spawnSync 2 | 3 | var styles = { 4 | // got these from playing around with what I found from: 5 | // https://github.com/istanbuljs/istanbuljs/blob/0f328fd0896417ccb2085f4b7888dd8e167ba3fa/packages/istanbul-lib-report/lib/file-writer.js#L84-L96 6 | // they're the best I could find that works well for light or dark terminals 7 | success: {open: '\u001b[32;1m', close: '\u001b[0m'}, 8 | danger: {open: '\u001b[31;1m', close: '\u001b[0m'}, 9 | info: {open: '\u001b[36;1m', close: '\u001b[0m'}, 10 | subtitle: {open: '\u001b[2;1m', close: '\u001b[0m'}, 11 | } 12 | 13 | function color(modifier, string) { 14 | return styles[modifier].open + string + styles[modifier].close 15 | } 16 | 17 | console.log(color('info', '▶️ Starting workshop setup...')) 18 | 19 | var output = spawnSync('npm --version', {shell: true}).stdout.toString().trim() 20 | var outputParts = output.split('.') 21 | var major = Number(outputParts[0]) 22 | var minor = Number(outputParts[1]) 23 | if (major < 8 || (major === 8 && minor < 16)) { 24 | console.error( 25 | color( 26 | 'danger', 27 | '🚨 npm version is ' + 28 | output + 29 | ' which is out of date. Please install npm@8.16.0 or greater', 30 | ), 31 | ) 32 | throw new Error('npm version is out of date') 33 | } 34 | 35 | var command = 36 | 'npx "https://gist.github.com/kentcdodds/bb452ffe53a5caa3600197e1d8005733" -q' 37 | console.log( 38 | color('subtitle', ' Running the following command: ' + command), 39 | ) 40 | 41 | var result = spawnSync(command, {stdio: 'inherit', shell: true}) 42 | 43 | if (result.status === 0) { 44 | console.log(color('success', '✅ Workshop setup complete...')) 45 | } else { 46 | process.exit(result.status) 47 | } 48 | 49 | /* 50 | eslint 51 | no-var: "off", 52 | "vars-on-top": "off", 53 | */ 54 | -------------------------------------------------------------------------------- /scripts/update-deps: -------------------------------------------------------------------------------- 1 | # prettier-ignore 2 | npx npm-check-updates --upgrade --reject husky,recoil,d3-geo,chalk,@jackfranklin/test-data-bot 3 | rm -rf node_modules package-lock.json 4 | npx npm@8 install 5 | npm run validate 6 | -------------------------------------------------------------------------------- /setup.js: -------------------------------------------------------------------------------- 1 | require('./scripts/setup') 2 | 3 | -------------------------------------------------------------------------------- /src/__tests__/01.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import {alfredTip} from '@kentcdodds/react-workshop-app/test-utils' 3 | import {render, screen} from '@testing-library/react' 4 | import userEvent from '@testing-library/user-event' 5 | import App from '../final/01' 6 | // import App from '../exercise/01' 7 | 8 | beforeEach(() => { 9 | window.navigator.geolocation = { 10 | getCurrentPosition: async () => ({ 11 | coords: { 12 | longitude: 321, 13 | latitude: 123, 14 | }, 15 | }), 16 | } 17 | }) 18 | 19 | test('loads the globe component asynchronously', async () => { 20 | render() 21 | 22 | await userEvent.click(screen.getByLabelText(/show globe/)) 23 | 24 | alfredTip( 25 | () => expect(screen.queryByTitle(/globe/i)).not.toBeInTheDocument(), 26 | 'The globe component must be loaded asynchronously via React.lazy and React.Suspense', 27 | ) 28 | 29 | expect(await screen.findByTitle(/globe/i)).toBeInTheDocument() 30 | }) 31 | -------------------------------------------------------------------------------- /src/__tests__/02.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import {alfredTip} from '@kentcdodds/react-workshop-app/test-utils' 3 | import {render, screen, fireEvent} from '@testing-library/react' 4 | import {getItems} from '../filter-cities' 5 | import App from '../final/02' 6 | // import App from '../exercise/02' 7 | 8 | jest.mock('../filter-cities') 9 | 10 | beforeEach(() => { 11 | const filterCities = jest.requireActual('../filter-cities') 12 | getItems.mockImplementation((...args) => { 13 | return filterCities.getItems(...args) 14 | }) 15 | }) 16 | 17 | test('useMemo is called properly', async () => { 18 | const {container} = render() 19 | const forceRerender = screen.getByText(/force rerender/i) 20 | 21 | alfredTip( 22 | () => expect(getItems).toHaveBeenCalledWith(''), 23 | 'The Menu component must call getItems with the inputValue.', 24 | ) 25 | 26 | getItems.mockClear() 27 | const findCity = screen.getByRole('textbox', {name: /find a city/i}) 28 | const filter = 'NO_CITY_WILL_MATCH_THIS' 29 | // using fireEvent because we only want 1 change event/re-render here 30 | fireEvent.change(findCity, {target: {value: filter}}) 31 | 32 | alfredTip( 33 | () => expect(container.querySelectorAll('li')).toHaveLength(0), 34 | `There are search results when there shouldn't be. Make sure to pass the inputValue into the dependecy array of the useMemo call. If you're doing that correctly, then make sure that you're calling the getItems function correctly.`, 35 | ) 36 | expect(getItems).toHaveBeenCalledWith(filter) 37 | alfredTip( 38 | () => expect(getItems).toHaveBeenCalledTimes(1), 39 | 'getItems was called even though the inputValue was unchanged. Make sure to wrap it in useMemo with the inputValue as a dependency.', 40 | ) 41 | 42 | getItems.mockClear() 43 | // using fireEvent because we do not want to blur the input 44 | // because downshift will set the input value to an empty string 45 | // on blur. 46 | fireEvent.click(forceRerender) 47 | alfredTip( 48 | () => expect(getItems).toHaveBeenCalledTimes(0), 49 | 'getItems was called even though the inputValue was unchanged. Make sure to wrap it in useMemo with the inputValue as a dependency.', 50 | ) 51 | }) 52 | -------------------------------------------------------------------------------- /src/__tests__/03.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import chalk from 'chalk' 3 | import '../final/03' 4 | // import '../final/03-extra.1' 5 | // import '../exercise/03' 6 | 7 | // this gets set as soon as we import the file 8 | // storing it here so it persists between tests 9 | const memoCalls = [...React.memo.mock.calls] 10 | 11 | jest.mock('../workerized-filter-cities', () => ({ 12 | getItems: jest.fn(() => { 13 | throw new Error('getItems must be mocked') 14 | }), 15 | })) 16 | 17 | jest.mock('react', () => { 18 | const actualReact = jest.requireActual('react') 19 | return { 20 | ...actualReact, 21 | memo: jest.fn((...args) => actualReact.memo(...args)), 22 | } 23 | }) 24 | 25 | test('Components are memoized', async () => { 26 | const memoizedFunctions = memoCalls.map(call => call[0].name) 27 | try { 28 | expect(memoizedFunctions).toContain('Menu') 29 | expect(memoizedFunctions).toContain('ListItem') 30 | if (memoCalls.length > 2) { 31 | expect(memoizedFunctions).toContain('Downshift') 32 | } 33 | } catch (error) { 34 | // 35 | // 36 | // 37 | // these comment lines are just here to keep the next line out of the codeframe 38 | // so it doesn't confuse people when they see the error message twice. 39 | if (memoizedFunctions.length < 2) { 40 | console.warn( 41 | `You may be seeing this error because the name of the function was removed (like this: const Menu = React.memo(() => {})). It's avised to keep the function name to improve the devtools experience (like this: const Menu = React.memo(function Menu() {}))`, 42 | ) 43 | } 44 | error.message = `🚨 ${chalk.red( 45 | `The Menu and ListItem components need to both be wrapped in React.memo. You do not need to have any other components memoized.`, 46 | )}\n\n${error.message}` 47 | 48 | throw error 49 | } 50 | }) 51 | -------------------------------------------------------------------------------- /src/__tests__/04.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import {alfredTip} from '@kentcdodds/react-workshop-app/test-utils' 3 | import {render, waitFor, screen} from '@testing-library/react' 4 | import {build, fake, sequence} from '@jackfranklin/test-data-bot' 5 | import {getItems} from '../workerized-filter-cities' 6 | import App from '../final/04' 7 | // import App from '../exercise/04' 8 | 9 | const buildItem = build({ 10 | fields: { 11 | id: sequence(), 12 | name: fake(f => f.name.firstName()), 13 | }, 14 | }) 15 | 16 | jest.mock('../workerized-filter-cities', () => ({ 17 | getItems: jest.fn(() => { 18 | throw new Error('getItems must be mocked') 19 | }), 20 | })) 21 | 22 | test('windows properly', async () => { 23 | const fakeItems = Array.from({length: 100}, () => buildItem()) 24 | const promise = Promise.resolve(fakeItems) 25 | getItems.mockReturnValue(promise) 26 | render() 27 | 28 | await waitFor(() => promise) 29 | 30 | const options = await screen.findAllByRole('option') 31 | 32 | alfredTip( 33 | () => expect(options.length).toBeLessThan(fakeItems.length), 34 | `Looks like all of the items are being rendered. Make sure you're using useVirtual and you're mapping over the virtualRows rather than the actual items.`, 35 | ) 36 | }) 37 | -------------------------------------------------------------------------------- /src/__tests__/05.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import {alfredTip} from '@kentcdodds/react-workshop-app/test-utils' 3 | import {render, screen} from '@testing-library/react' 4 | import userEvent from '@testing-library/user-event' 5 | import App from '../final/05' 6 | // import App from '../exercise/05' 7 | 8 | // sorry, I just couldn't find a reliable way to test your implementation 9 | // so this test just ensures you don't break anything 😅 10 | 11 | test('app continues to work', async () => { 12 | render() 13 | const dogNameInput = screen.getByRole('textbox', {name: /dog name/i}) 14 | await userEvent.type(dogNameInput, 'Gemma') 15 | alfredTip(() => { 16 | expect(screen.getByText('Gemma')).toBeInTheDocument() 17 | }, `Unable to type a dog name and have it printed out.`) 18 | 19 | const firstButton = document.body.querySelector('button.cell') 20 | const numberBefore = firstButton.textContent 21 | await userEvent.click(firstButton) 22 | let numberAfter = firstButton.textContent 23 | if (numberAfter === numberBefore) { 24 | // it's possible that the randomization logic came up with the same number 25 | // but it's much less likely that would happen twice 😅 26 | await userEvent.click(firstButton) 27 | numberAfter = firstButton.textContent 28 | } 29 | alfredTip(() => { 30 | expect(numberAfter).not.toBe(numberBefore) 31 | }, `Unable to click the first cell to update its value.`) 32 | }, 10_000) 33 | -------------------------------------------------------------------------------- /src/__tests__/06.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import {alfredTip} from '@kentcdodds/react-workshop-app/test-utils' 3 | import {render, screen} from '@testing-library/react' 4 | import userEvent from '@testing-library/user-event' 5 | import App from '../final/06' 6 | // import App from '../exercise/06' 7 | 8 | // sorry, I just couldn't find a reliable way to test your implementation 9 | // so this test just ensures you don't break anything 😅 10 | 11 | test('app continues to work', async () => { 12 | render() 13 | const dogNameInput = screen.getByRole('textbox', {name: /dog name/i}) 14 | await userEvent.type(dogNameInput, 'Gemma') 15 | alfredTip(() => { 16 | expect(screen.getByText('Gemma')).toBeInTheDocument() 17 | }, `Unable to type a dog name and have it printed out.`) 18 | 19 | const firstButton = document.body.querySelector('button.cell') 20 | const numberBefore = firstButton.textContent 21 | await userEvent.click(firstButton) 22 | let numberAfter = firstButton.textContent 23 | if (numberAfter === numberBefore) { 24 | // it's possible that the randomization logic came up with the same number 25 | // but it's much less likely that would happen twice 😅 26 | await userEvent.click(firstButton) 27 | numberAfter = firstButton.textContent 28 | } 29 | alfredTip(() => { 30 | expect(numberAfter).not.toBe(numberBefore) 31 | }, `Unable to click the first cell to update its value.`) 32 | }) 33 | -------------------------------------------------------------------------------- /src/__tests__/07.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import {alfredTip} from '@kentcdodds/react-workshop-app/test-utils' 3 | import {render} from '@testing-library/react' 4 | import reportProfile from '../report-profile' 5 | import App from '../final/07' 6 | // import App from '../exercise/07' 7 | 8 | jest.mock('react', () => { 9 | const actualReact = jest.requireActual('react') 10 | return { 11 | ...actualReact, 12 | Profiler: jest.fn(), 13 | } 14 | }) 15 | 16 | beforeEach(() => { 17 | React.Profiler.mockImplementation(({children}) => children) 18 | }) 19 | 20 | test('uses the Profiler correctly', async () => { 21 | render() 22 | 23 | alfredTip( 24 | () => 25 | expect(React.Profiler).toHaveBeenLastCalledWith( 26 | { 27 | children: expect.any(Object), 28 | id: 'counter', 29 | onRender: reportProfile, 30 | }, 31 | expect.any(Object), 32 | ), 33 | 'The React.Profiler component must be used with the correct props', 34 | ) 35 | }) 36 | -------------------------------------------------------------------------------- /src/examples/code-splitting/deps-included/dep.js: -------------------------------------------------------------------------------- 1 | // http://localhost:3000/isolated/examples/code-splitting/deps-included/dep.js 2 | 3 | import * as React from 'react' 4 | 5 | function Dep() { 6 | return
Hello from a dependency!
7 | } 8 | export default Dep 9 | -------------------------------------------------------------------------------- /src/examples/code-splitting/deps-included/index.js: -------------------------------------------------------------------------------- 1 | // http://localhost:3000/isolated/examples/code-splitting/deps-included/index.js 2 | 3 | import * as React from 'react' 4 | import Dep from './dep' 5 | 6 | function DepsIncluded() { 7 | return 8 | } 9 | 10 | export default DepsIncluded 11 | -------------------------------------------------------------------------------- /src/examples/code-splitting/group/one.js: -------------------------------------------------------------------------------- 1 | // http://localhost:3000/isolated/examples/code-splitting/group/one.js 2 | 3 | import * as React from 'react' 4 | 5 | function One() { 6 | return
One
7 | } 8 | export default One 9 | -------------------------------------------------------------------------------- /src/examples/code-splitting/group/two.js: -------------------------------------------------------------------------------- 1 | // http://localhost:3000/isolated/examples/code-splitting/group/two.js 2 | 3 | import * as React from 'react' 4 | 5 | function Two() { 6 | return
Two
7 | } 8 | export default Two 9 | -------------------------------------------------------------------------------- /src/examples/code-splitting/main.js: -------------------------------------------------------------------------------- 1 | // http://localhost:3000/isolated/examples/code-splitting/main.js 2 | 3 | import * as React from 'react' 4 | 5 | const DepsIncluded = React.lazy(() => 6 | import(/* webpackChunkName: "deps" */ './deps-included'), 7 | ) 8 | const One = React.lazy(() => 9 | import(/* webpackChunkName: "group" */ './group/one'), 10 | ) 11 | const Two = React.lazy(() => 12 | import(/* webpackChunkName: "group" */ './group/two'), 13 | ) 14 | 15 | const Prefetched = React.lazy(() => 16 | import( 17 | /* webpackPrefetch: true */ 18 | /* webpackChunkName: "prefetched" */ 19 | './prefetched' 20 | ), 21 | ) 22 | const Preloaded = React.lazy(() => 23 | import( 24 | /* webpackPreload: true */ 25 | /* webpackChunkName: "preload" */ 26 | './preloaded' 27 | ), 28 | ) 29 | 30 | function Main() { 31 | const [show, setShow] = React.useState(false) 32 | return ( 33 | 34 | 35 | {show ? ( 36 |
37 | 38 | 39 | 40 | 41 | 42 |
43 | ) : null} 44 |
45 | ) 46 | } 47 | 48 | export default Main 49 | -------------------------------------------------------------------------------- /src/examples/code-splitting/prefetched.js: -------------------------------------------------------------------------------- 1 | // http://localhost:3000/isolated/examples/code-splitting/prefetched.js 2 | 3 | import * as React from 'react' 4 | 5 | function Prefetched() { 6 | return
Prefetched module
7 | } 8 | 9 | export default Prefetched 10 | -------------------------------------------------------------------------------- /src/examples/code-splitting/preloaded.js: -------------------------------------------------------------------------------- 1 | // http://localhost:3000/isolated/examples/code-splitting/preloaded.js 2 | 3 | import * as React from 'react' 4 | 5 | function Preloaded() { 6 | return
Preloaded module
7 | } 8 | 9 | export default Preloaded 10 | -------------------------------------------------------------------------------- /src/examples/unnecessary-rerenders.js: -------------------------------------------------------------------------------- 1 | // http://localhost:3000/isolated/examples/unnecessary-rerenders.js 2 | 3 | import * as React from 'react' 4 | 5 | function CountButton({count, onClick}) { 6 | return 7 | } 8 | 9 | function NameInput({name, onNameChange}) { 10 | return ( 11 | 14 | ) 15 | } 16 | 17 | function Example() { 18 | const [name, setName] = React.useState('') 19 | const [count, setCount] = React.useState(0) 20 | const increment = () => setCount(c => c + 1) 21 | return ( 22 |
23 |
24 | 25 |
26 |
27 | 28 |
29 | {name ?
{`${name}'s favorite number is ${count}`}
: null} 30 |
31 | ) 32 | } 33 | 34 | export default Example 35 | 36 | /* 37 | eslint 38 | no-func-assign: 0, 39 | */ 40 | -------------------------------------------------------------------------------- /src/exercise/01.js: -------------------------------------------------------------------------------- 1 | // Code splitting 2 | // http://localhost:3000/isolated/exercise/01.js 3 | 4 | import * as React from 'react' 5 | // 💣 remove this import 6 | import Globe from '../globe' 7 | 8 | // 🐨 use React.lazy to create a Globe component which uses a dynamic import 9 | // to get the Globe component from the '../globe' module. 10 | 11 | function App() { 12 | const [showGlobe, setShowGlobe] = React.useState(false) 13 | 14 | // 🐨 wrap the code below in a component 15 | // with a fallback. 16 | // 💰 try putting it in a few different places and observe how that 17 | // impacts the user experience. 18 | return ( 19 |
29 | 37 |
38 | {showGlobe ? : null} 39 |
40 |
41 | ) 42 | } 43 | // 🦉 Note that if you're not on the isolated page, then you'll notice that this 44 | // app actually already has a React.Suspense component higher up in the tree 45 | // where this component is rendered, so you *could* just rely on that one. 46 | 47 | export default App 48 | -------------------------------------------------------------------------------- /src/exercise/01.md: -------------------------------------------------------------------------------- 1 | # Code splitting 2 | 3 | ## 📝 Your Notes 4 | 5 | Elaborate on your learnings here in `src/exercise/01.md` 6 | 7 | ## Background 8 | 9 | Code splitting acts on the principle that loading less code will speed up your 10 | app. Say for example that we're building a complex dashboard application that 11 | includes the venerable d3 library for graphing data. Your users start 12 | complaining because it takes too long to load the login screen. 13 | 14 | So, considering that performance problems can be resolved by less code, how can we 15 | solve this one? Well, do we really _need_ to have that code for the chart when 16 | the user loads the login screen? Nope! We could load that on-demand. 17 | 18 | Luckily for us, there's a built-in way to do this with JavaScript standards. 19 | It's called a dynamic import and the syntax looks like this: 20 | 21 | ```javascript 22 | import('/some-module.js').then( 23 | module => { 24 | // do stuff with the module's exports 25 | }, 26 | error => { 27 | // there was some error loading the module... 28 | }, 29 | ) 30 | ``` 31 | 32 | > 📜 Learn more about dynamic imports in the browser in 33 | > [Super Simple Start to ESModules in the browser](https://kentcdodds.com/blog/super-simple-start-to-es-modules-in-the-browser) 34 | 35 | To take this further, React has built-in support for loading modules as React 36 | components. The module must have a React component as the default export, and 37 | you have to use the `` component to render a fallback value 38 | while the user waits for the module to be loaded. 39 | 40 | ```javascript 41 | // smiley-face.js 42 | import * as React from 'react' 43 | 44 | function SmileyFace() { 45 | return
😃
46 | } 47 | 48 | export default SmileyFace 49 | 50 | // app.js 51 | import * as React from 'react' 52 | 53 | const SmileyFace = React.lazy(() => import('./smiley-face')) 54 | 55 | function App() { 56 | return ( 57 |
58 | loading...
}> 59 | 60 |
61 | 62 | ) 63 | } 64 | ``` 65 | 66 | 🦉 One great way to analyze your app to determine the need/benefit of code 67 | splitting for a certain feature/page/interaction, is to use 68 | [the "Coverage" feature of the developer tools](https://developer.chrome.com/docs/devtools/coverage). 69 | 70 | ## Exercise 71 | 72 | Production deploys: 73 | 74 | - [Exercise](https://react-performance.netlify.app/isolated/exercise/01.js) 75 | - [Final](https://react-performance.netlify.app/isolated/final/01.js) 76 | 77 | Our app has a neat Globe component that shows the user where they are on the 78 | globe. Cool right? It's super duper fun. 79 | 80 | But one day our product manager 👨‍💼 came along and said that users are 81 | complaining the app is taking too long to load. We're using several sizeable 82 | libraries to have the really cool globe, but users only need to load it if they 83 | click the "show globe" button and loading it ahead of time makes the app load 84 | slower. 85 | 86 | So your job as a performance professional is to load the code on-demand so the 87 | user doesn't have to wait to see the checkbox. 88 | 89 | For this one, you'll need to open the final in isolation and open the Chrome 90 | DevTools Network tab to watch the webpack chunks load when you click "show 91 | globe." Your objective is to have the network load those same chunks so they're 92 | not in the bundle to begin with. 93 | 94 | 💰 Here's a quick tip: In the Network tab, there's a dropdown for artificially 95 | throttling your network speed. It defaults to "Online" but you can change it to 96 | "Fast 3G", "Slow 3G", etc. 97 | 98 | Also, spend a bit of time playing with the coverage feature of the dev tools (as 99 | noted above). 100 | 101 | 🦉 You may also want to try running the production build so you can see what the 102 | sizes are like post-minification: Run `npm run build` and then `npm run serve`. 103 | 104 | 🦉 You may also want to use Incognito mode so your browser plugins don't mess 105 | with the typical user experience. 106 | 107 | ## Extra Credit 108 | 109 | ### 1. 💯 eager loading 110 | 111 | [Production deploy](https://react-performance.netlify.app/isolated/final/01.extra-1.js) 112 | 113 | So it's great that the users can get the app loaded faster, but it's annoying 114 | when 99% of the time the reason the users are using the app is so they can 115 | interact with our globe. We don't want to have to make them wait first to load 116 | the app and then again to load the globe. Wouldn't it be cool if we could have 117 | globe start loading as soon as the user hovers over the checkbox? So if they 118 | `mouseOver` or `focus` the `