├── .gitignore ├── .nvmrc ├── LICENSE ├── README.md ├── package-lock.json ├── package.json ├── public ├── assets │ ├── .DS_Store │ ├── css │ │ ├── banner.css │ │ ├── index.css │ │ └── main.css │ ├── images │ │ ├── callouts │ │ │ ├── .DS_Store │ │ │ ├── callout-breakdown-1200.png │ │ │ ├── callout-breakdown-2400.png │ │ │ ├── callout-breakdown-600.png │ │ │ ├── callout-breakdown-900.png │ │ │ ├── callout-distribution-1200.png │ │ │ ├── callout-distribution-2400.png │ │ │ ├── callout-distribution-600.png │ │ │ ├── callout-distribution-900.png │ │ │ ├── callout-webvital-1200.png │ │ │ ├── callout-webvital-2400.png │ │ │ ├── callout-webvital-600.png │ │ │ └── callout-webvital-900.png │ │ ├── checkmark-circle.svg │ │ ├── copy.svg │ │ ├── favicon │ │ │ ├── android-chrome-192x192.png │ │ │ ├── android-chrome-512x512.png │ │ │ ├── apple-touch-icon.png │ │ │ ├── favicon-16x16.png │ │ │ ├── favicon-32x32.png │ │ │ ├── favicon.ico │ │ │ └── site.webmanifest.json │ │ ├── illustrations │ │ │ ├── .DS_Store │ │ │ ├── celebrating_minion.svg │ │ │ ├── chasing_after_rocket_square.svg │ │ │ ├── flying_sloth_2.png │ │ │ ├── sloth_building_rocket.svg │ │ │ ├── sloth_concerned.svg │ │ │ ├── sloth_missing_rocket.svg │ │ │ ├── sloth_paper_airplane.svg │ │ │ ├── sloth_pricing_rocket.svg │ │ │ ├── sloth_rocket_launch.svg │ │ │ └── web_performance.svg │ │ ├── request_metrics_logo.png │ │ ├── request_metrics_logo_dark.svg │ │ ├── request_metrics_logo_rss.png │ │ ├── share.min.png │ │ ├── xmark-circle.svg │ │ └── youtube_avatar_transparent_40x40.png │ ├── js │ │ ├── banner.js │ │ └── util │ │ │ ├── lazyloader.js │ │ │ └── perf.js │ └── vendor │ │ ├── chatty │ │ ├── avatar.png │ │ ├── bulky.js │ │ ├── chatty.css │ │ ├── chatty.js │ │ └── index.html │ │ ├── fontello │ │ ├── LICENSE.txt │ │ ├── README.txt │ │ ├── config.json │ │ ├── css │ │ │ ├── animation.css │ │ │ ├── fontello-codes.css │ │ │ ├── fontello-embedded.css │ │ │ ├── fontello-ie7-codes.css │ │ │ ├── fontello-ie7.css │ │ │ └── fontello.css │ │ ├── demo.html │ │ └── font │ │ │ ├── fontello.eot │ │ │ ├── fontello.svg │ │ │ ├── fontello.ttf │ │ │ ├── fontello.woff │ │ │ └── fontello.woff2 │ │ └── lite-youtube │ │ ├── lite-youtube.map │ │ └── lite-youtube.min.js ├── favicon.ico └── index.html ├── server └── server.js ├── slide_title.png └── tools └── imageOptimizer.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | npm-debug.log* 3 | .npm 4 | .DS_Store 5 | logs 6 | public/assets/images/**/*.min.* 7 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 18.12.1 -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Todd H. Gardner 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Fundamentals of Web Performance Demo Website 2 | 3 |

4 | 5 |
6 |

7 | 8 | This is example website for Todd Gardner's "Fundamentals of Web Performance" workshop. You can find he workshop online at [Frontend Masters](https://frontendmasters.com/courses/web-perf/). It is intended to illustrate common and effective techniques to improve the performance of a website. It is not meant to be used in production. 9 | 10 | ## How to use 11 | 12 | ``` 13 | npm install 14 | npm start 15 | ``` 16 | 17 | Open a browser at [http://localhost:3000/](http://localhost:3000/) to view the website. 18 | 19 | ## Related Resources: 20 | 21 | - [Slides and Materials](https://drive.google.com/drive/folders/13sdKqO8O2L1yr6th9HgPwpncZwTpEPGl?usp=sharing) 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "perf-training-website", 3 | "version": "2.0.0", 4 | "description": "Example website for 'Fundamentals of Web Performance' workshop", 5 | "main": "server/server.js", 6 | "scripts": { 7 | "imagemin": "node ./tools/imageOptimizer.js", 8 | "start": "node server/server.js", 9 | "test": "echo \"Error: no test specified\" && exit 1" 10 | }, 11 | "config": { 12 | "server-port": 3000, 13 | "server-delay": 200, 14 | "server-compress": false 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "git+https://github.com/toddhgardner/perf-training-website.git" 19 | }, 20 | "keywords": [ 21 | "core web vitals", 22 | "web vitals", 23 | "largest contentful paint", 24 | "first input delay", 25 | "cumulative layout shift" 26 | ], 27 | "author": "Todd Gardner ", 28 | "license": "MIT", 29 | "bugs": { 30 | "url": "https://github.com/toddhgardner/perf-training-website/issues" 31 | }, 32 | "homepage": "https://github.com/toddhgardner/perf-training-website#readme", 33 | "dependencies": { 34 | "body-parser": "1.20.1", 35 | "compression": "1.7.4", 36 | "express": "4.18.2", 37 | "imagemin": "7.0.1", 38 | "imagemin-gifsicle": "7.0.0", 39 | "imagemin-jpegtran": "7.0.0", 40 | "imagemin-pngquant": "9.0.1", 41 | "imagemin-svgo": "8.0.0" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /public/assets/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toddhgardner/perf-training-website/781e0d80838572d19c124924d61f05c445ea13f8/public/assets/.DS_Store -------------------------------------------------------------------------------- /public/assets/css/banner.css: -------------------------------------------------------------------------------- 1 | .consent-banner { 2 | background-color: #182368; 3 | min-height: 240px; 4 | left: 0; 5 | right: 0; 6 | z-index: 9999; 7 | 8 | position: relative; 9 | top: 0; 10 | 11 | /** 12 | position: fixed; 13 | bottom: 0; 14 | **/ 15 | } 16 | .consent-banner .container { 17 | max-width: 1200px; 18 | width: 100%; 19 | margin: 0 auto; 20 | padding: 20px; 21 | display: flex; 22 | flex-direction: row; 23 | height: 100%; 24 | min-height: 240px; 25 | align-items: center; 26 | } 27 | .consent-banner .container .copy { 28 | flex: 1 1; 29 | padding: 10px; 30 | } 31 | .consent-banner .container .copy p { 32 | font-size: 1.2em; 33 | color: white; 34 | max-width: 600px; 35 | } 36 | .consent-banner .container .control { 37 | flex: 0 0 200px; 38 | } 39 | @media(max-width: 600px) { 40 | .consent-banner .container { 41 | flex-direction: column; 42 | } 43 | .consent-banner .container .control { 44 | flex: 0 0 auto; 45 | } 46 | } -------------------------------------------------------------------------------- /public/assets/css/main.css: -------------------------------------------------------------------------------- 1 | @import url(/assets/vendor/fontello/css/fontello.css); 2 | @import url(/assets/css/banner.css); 3 | @import url(/assets/css/index.css); 4 | 5 | * { box-sizing: border-box; } 6 | 7 | /** Reset some basic elements */ 8 | body, h1, h2, h3, h4, h5, h6, p, blockquote, pre, hr, dl, dd, ol, ul, figure { margin: 0; padding: 0; } 9 | 10 | /** Basic styling */ 11 | body { font: 400 16px/1.5 "Muli", Helvetica, Arial, sans-serif; color: #2E4158; background-color: #fafbfe; -webkit-text-size-adjust: 100%; -webkit-font-feature-settings: "kern" 1; -moz-font-feature-settings: "kern" 1; -o-font-feature-settings: "kern" 1; font-feature-settings: "kern" 1; font-kerning: normal; display: flex; min-height: 100vh; flex-direction: column; width: 100%; overscroll-behavior-x: none; } 12 | 13 | /** Set `margin-bottom` to maintain vertical rhythm */ 14 | h1, h2, h3, h4, h5, h6, p, blockquote, pre, ul, ol, dl, figure { margin-bottom: 15px; } 15 | 16 | h1, h2, h3, h4, h5, h6 { font: 600 16px/1.5 "Poppins", Helvetica, Arial, sans-serif; } 17 | 18 | /** `main` element */ 19 | main { display: block; /* Default value of `display` of `main` element is 'inline' in IE 11. */ } 20 | 21 | /** Images */ 22 | img { max-width: 100%; height: auto; vertical-align: middle; } 23 | 24 | /** Lists */ 25 | li > ul, li > ol { margin-bottom: 0; } 26 | 27 | /** Headings */ 28 | h1, h2, h3, h4, h5, h6 { font-weight: bold; font-family: "Poppins", Helvetica, Arial, sans-serif; } 29 | 30 | h2 { font-size: 1.2em; } 31 | 32 | /** Links */ 33 | a { color: #0E63F4; text-decoration: none; } 34 | a:hover { color: #2E4158; text-decoration: underline; } 35 | .social-media-list a:hover { text-decoration: none; } 36 | .social-media-list a:hover .username { text-decoration: underline; } 37 | 38 | .container { width: 100%; padding-right: 15px; padding-left: 15px; margin-right: auto; margin-left: auto; display: flex; max-width: 1200px; min-width: 380px; } 39 | @media (max-width: 900px) { .container { max-width: 900px; } } 40 | @media (max-width: 768px) { .container { max-width: 768px; } } 41 | @media (max-width: 600px) { .container { max-width: 600px; } } 42 | 43 | .relative { position: relative; } 44 | 45 | .flex { display: flex; flex-direction: row; } 46 | 47 | .flex-column { display: flex; flex-direction: column; } 48 | 49 | .justify-center { justify-content: center; } 50 | 51 | .align-center { align-items: center; } 52 | 53 | .text-center { text-align: center; } 54 | 55 | .text-width { max-width: 700px; } 56 | 57 | .btn { border: none; padding: 6px 12px; display: inline-block; text-align: center; border-radius: 6px; text-decoration: none; cursor: pointer; font-family: "Muli", Helvetica, Arial, sans-serif; font-size: 1em; line-height: 1.5em; } 58 | .btn:active, .btn:hover, .btn:focus { text-decoration: none; } 59 | .btn.btn-big { padding: 14px 30px; font-size: 1.2em; font-weight: 600; } 60 | .btn.btn-small { padding: 3px 8px; font-size: 0.85em; } 61 | .btn.btn-grey { border: 1px solid #8C9AA9; background-color: #fafbfe; color: #0E63F4; } 62 | .btn.btn-grey:active, .btn.btn-grey:hover, .btn.btn-grey:focus { background-color: #c7d2f4; } 63 | .btn.btn-green { background-color: #33C19F; color: white; } 64 | .btn.btn-green:active, .btn.btn-green:hover, .btn.btn-green:focus { background-color: #269177; } 65 | .btn.btn-blue { background-color: #0E63F4; color: white; } 66 | .btn.btn-blue:active, .btn.btn-blue:hover, .btn.btn-blue:focus { background-color: #094bbc; } 67 | .btn.btn-blue-inverse { background-color: white; color: #0E63F4; border: 1px solid #0E63F4; } 68 | .btn.btn-blue-inverse:active, .btn.btn-blue-inverse:hover, .btn.btn-blue-inverse:focus { background-color: #e0e0e0; } 69 | .btn[disabled] { background-color: #e5eafa; color: #8C9AA9; } 70 | .btn[disabled]:active, .btn[disabled]:hover, .btn[disabled]:focus { background-color: #e5eafa; color: #8C9AA9; } 71 | 72 | header.site-header .container { height: 88px; } 73 | header.site-header .container .site-logo { padding: 12px 0; flex: 0 0 auto; } 74 | header.site-header .container .site-logo img { height: 64px; } 75 | header.site-header .container nav.site-nav { flex: 1 1 auto; } 76 | header.site-header .container nav.site-nav a { display: block; height: 64px; line-height: 64px; font-family: "Poppins", Helvetica, Arial, sans-serif; font-size: 0.9em; padding: 0 20px; } 77 | header.site-header .container nav.site-nav .site-links { padding: 12px 0 12px 20px; } 78 | header.site-header .container nav.site-nav .site-links a { margin-right: 20px; color: #2E4158; } 79 | header.site-header .container nav.site-nav .site-links a.current { font-weight: 600; } 80 | header.site-header .container nav.site-nav .site-cta { padding: 12px 0; } 81 | header.site-header .container nav.site-nav .site-cta a.signin { font-weight: 600; } 82 | header.site-header .container nav.site-nav .site-cta a.signup { line-height: 1em; padding: 15px 25px; margin: 10px 0 10px 20px; height: auto; } 83 | header.site-header .container nav.site-nav input[type=checkbox] { position: absolute; opacity: 0; } 84 | header.site-header .container nav.site-nav .spacer { display: none; } 85 | header.site-header .container nav.site-nav .hamburger, header.site-header .container nav.site-nav .hamburger-close { display: none; } 86 | header.site-header .container nav.site-nav .drawer { height: 100%; width: 100%; display: flex; justify-content: space-between; } 87 | @media (max-width: 1100px) { header.site-header .container { justify-content: space-between; } 88 | header.site-header .container .spacer { display: block; width: 50px; } 89 | header.site-header .container nav.site-nav { flex: none; } 90 | header.site-header .container nav.site-nav .hamburger, header.site-header .container nav.site-nav .hamburger-close { display: block; height: 100%; width: 50px; display: flex; justify-content: center; align-items: center; } 91 | header.site-header .container nav.site-nav .hamburger .icon, header.site-header .container nav.site-nav .hamburger-close .icon { font-size: 2.5em; } 92 | header.site-header .container nav.site-nav .hamburger-close { display: none; } 93 | header.site-header .container nav.site-nav .drawer { background: white; font-size: 1.2em; display: block; height: auto; position: absolute; z-index: 2; top: 88px; background: white; border-bottom: 2px solid #DDE4EB; left: 0; right: 0; overflow: hidden; max-height: 0; transition: 200ms max-height ease-out; } 94 | header.site-header .container nav.site-nav .drawer .site-links, header.site-header .container nav.site-nav .drawer .site-cta { border-top: 1px solid #DDE4EB; flex-direction: column; padding: 0; } 95 | header.site-header .container nav.site-nav .drawer a { display: block; } 96 | header.site-header .container nav.site-nav .drawer a.signup { margin: 20px; } 97 | header.site-header .container nav.site-nav #drawer-activator:checked ~ .drawer { max-height: 350px; } 98 | header.site-header .container nav.site-nav #drawer-activator:checked ~ .hamburger { display: none; } 99 | header.site-header .container nav.site-nav #drawer-activator:checked ~ .hamburger-close { display: flex; } } 100 | 101 | figure.code { margin: 15px 0; } 102 | figure.code .code-wrap { position: relative; border: 1px solid #CCD2D8; border-radius: 6px; overflow: hidden; } 103 | figure.code pre { min-height: 80px; margin-bottom: 0; padding: 10px; overflow-x: auto; white-space: pre; word-wrap: initial; background-color: #212932; color: white; } 104 | figure.code.oneliner pre { padding: 26px; } 105 | figure.code .copy-button { border: 1px solid white; position: absolute; right: 20px; top: 20px; padding: 8px 18px; border-radius: 4px; cursor: pointer; background-color: transparent; color: white; } 106 | figure.code .copy-button span { padding-left: 3px; font-size: 18px; } 107 | figure.code .copy-button img { width: 20px; vertical-align: top; margin: 0; border: none; display: initial; } 108 | figure.code .copy-button:hover, figure.code .copy-button:focus, figure.code .copy-button:active { background-color: #00bbe6; color: white; } 109 | figure.code figcaption { font-family: "Muli", Helvetica, Arial, sans-serif; border-top: 1px solid grey; font-size: 12px; text-align: center; padding: 5px; background-color: #212932; line-height: 26px; } 110 | figure.code figcaption a { color: #e8e8e8; font-size: 14px; text-align: center; padding: 5px; font-weight: 500; text-decoration: none; } 111 | figure.code figcaption a:hover, figure.code figcaption a:active, figure.code figcaption a:focus { color: #00bbe6; } 112 | 113 | /* https://github.com/Microsoft/vscode/blob/master/extensions/theme-defaults/themes/light_vs.json */ 114 | pre.prettyprint .pln { color: #9CDCFE; } 115 | pre.prettyprint .str { color: #ce9178; } 116 | pre.prettyprint .kwd { color: #569cd6; } 117 | pre.prettyprint .com { color: #608b4e; font-style: italic; } 118 | pre.prettyprint .typ { color: #4EC9B0; } 119 | pre.prettyprint .lit { color: #0ddb90; } 120 | pre.prettyprint .clo, pre.prettyprint .opn, pre.prettyprint .pun { color: #D4D4D4; } 121 | pre.prettyprint .tag { color: #569CD6; } 122 | pre.prettyprint .atn { color: #9CDCFE; } 123 | pre.prettyprint .atv { color: #CE9178; } 124 | pre.prettyprint .dec, pre.prettyprint .var { color: #B5CEA8; } 125 | pre.prettyprint .fun { color: #569cd6; } 126 | 127 | code.language-plaintext { font-family: monospace; background-color: #fff; border: 1px solid #ddd; padding: 0 8px; border-radius: 3px; color: #2D689E; } 128 | 129 | footer.site-footer { background-color: #182368; color: #9ba7b4; } 130 | footer.site-footer a { color: #fafbfe; } 131 | footer.site-footer .container { padding: 60px 20px 0; } 132 | footer.site-footer .footer-about { flex: 1 1 33%; } 133 | footer.site-footer .footer-about .footer-logo { margin-bottom: 30px; } 134 | footer.site-footer .footer-about .footer-logo img { height: 64px; } 135 | footer.site-footer .footer-about .footer-social a { font-size: 3em; } 136 | footer.site-footer .footer-about .footer-social a:hover, footer.site-footer .footer-about .footer-social a:active, footer.site-footer .footer-about .footer-social a:focus { color: #d0d9f6; } 137 | footer.site-footer .footer-links { flex: 1 1 66%; margin-top: 60px; } 138 | footer.site-footer .footer-links .footer-links-1, footer.site-footer .footer-links .footer-links-2 { flex: 1; padding: 0 60px; } 139 | footer.site-footer .footer-links h3 { text-transform: uppercase; font-weight: 400; } 140 | footer.site-footer .footer-links a { padding: 0 0 30px; font-size: 1.1em; } 141 | footer.site-footer .legal { margin: 0 auto; } 142 | @media (max-width: 768px) { footer.site-footer .container { flex-direction: column; } 143 | footer.site-footer .footer-links .footer-links-1 { padding: 0 30px 0 0; } 144 | footer.site-footer .footer-links .footer-links-2 { padding: 0 0 0 30px; } } 145 | 146 | 147 | -------------------------------------------------------------------------------- /public/assets/images/callouts/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toddhgardner/perf-training-website/781e0d80838572d19c124924d61f05c445ea13f8/public/assets/images/callouts/.DS_Store -------------------------------------------------------------------------------- /public/assets/images/callouts/callout-breakdown-1200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toddhgardner/perf-training-website/781e0d80838572d19c124924d61f05c445ea13f8/public/assets/images/callouts/callout-breakdown-1200.png -------------------------------------------------------------------------------- /public/assets/images/callouts/callout-breakdown-2400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toddhgardner/perf-training-website/781e0d80838572d19c124924d61f05c445ea13f8/public/assets/images/callouts/callout-breakdown-2400.png -------------------------------------------------------------------------------- /public/assets/images/callouts/callout-breakdown-600.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toddhgardner/perf-training-website/781e0d80838572d19c124924d61f05c445ea13f8/public/assets/images/callouts/callout-breakdown-600.png -------------------------------------------------------------------------------- /public/assets/images/callouts/callout-breakdown-900.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toddhgardner/perf-training-website/781e0d80838572d19c124924d61f05c445ea13f8/public/assets/images/callouts/callout-breakdown-900.png -------------------------------------------------------------------------------- /public/assets/images/callouts/callout-distribution-1200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toddhgardner/perf-training-website/781e0d80838572d19c124924d61f05c445ea13f8/public/assets/images/callouts/callout-distribution-1200.png -------------------------------------------------------------------------------- /public/assets/images/callouts/callout-distribution-2400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toddhgardner/perf-training-website/781e0d80838572d19c124924d61f05c445ea13f8/public/assets/images/callouts/callout-distribution-2400.png -------------------------------------------------------------------------------- /public/assets/images/callouts/callout-distribution-600.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toddhgardner/perf-training-website/781e0d80838572d19c124924d61f05c445ea13f8/public/assets/images/callouts/callout-distribution-600.png -------------------------------------------------------------------------------- /public/assets/images/callouts/callout-distribution-900.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toddhgardner/perf-training-website/781e0d80838572d19c124924d61f05c445ea13f8/public/assets/images/callouts/callout-distribution-900.png -------------------------------------------------------------------------------- /public/assets/images/callouts/callout-webvital-1200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toddhgardner/perf-training-website/781e0d80838572d19c124924d61f05c445ea13f8/public/assets/images/callouts/callout-webvital-1200.png -------------------------------------------------------------------------------- /public/assets/images/callouts/callout-webvital-2400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toddhgardner/perf-training-website/781e0d80838572d19c124924d61f05c445ea13f8/public/assets/images/callouts/callout-webvital-2400.png -------------------------------------------------------------------------------- /public/assets/images/callouts/callout-webvital-600.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toddhgardner/perf-training-website/781e0d80838572d19c124924d61f05c445ea13f8/public/assets/images/callouts/callout-webvital-600.png -------------------------------------------------------------------------------- /public/assets/images/callouts/callout-webvital-900.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toddhgardner/perf-training-website/781e0d80838572d19c124924d61f05c445ea13f8/public/assets/images/callouts/callout-webvital-900.png -------------------------------------------------------------------------------- /public/assets/images/checkmark-circle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /public/assets/images/copy.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /public/assets/images/favicon/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toddhgardner/perf-training-website/781e0d80838572d19c124924d61f05c445ea13f8/public/assets/images/favicon/android-chrome-192x192.png -------------------------------------------------------------------------------- /public/assets/images/favicon/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toddhgardner/perf-training-website/781e0d80838572d19c124924d61f05c445ea13f8/public/assets/images/favicon/android-chrome-512x512.png -------------------------------------------------------------------------------- /public/assets/images/favicon/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toddhgardner/perf-training-website/781e0d80838572d19c124924d61f05c445ea13f8/public/assets/images/favicon/apple-touch-icon.png -------------------------------------------------------------------------------- /public/assets/images/favicon/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toddhgardner/perf-training-website/781e0d80838572d19c124924d61f05c445ea13f8/public/assets/images/favicon/favicon-16x16.png -------------------------------------------------------------------------------- /public/assets/images/favicon/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toddhgardner/perf-training-website/781e0d80838572d19c124924d61f05c445ea13f8/public/assets/images/favicon/favicon-32x32.png -------------------------------------------------------------------------------- /public/assets/images/favicon/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toddhgardner/perf-training-website/781e0d80838572d19c124924d61f05c445ea13f8/public/assets/images/favicon/favicon.ico -------------------------------------------------------------------------------- /public/assets/images/favicon/site.webmanifest.json: -------------------------------------------------------------------------------- 1 | {"name":"","short_name":"","icons":[{"src":"/assets/images/favicon/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/assets/images/favicon/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"} -------------------------------------------------------------------------------- /public/assets/images/illustrations/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toddhgardner/perf-training-website/781e0d80838572d19c124924d61f05c445ea13f8/public/assets/images/illustrations/.DS_Store -------------------------------------------------------------------------------- /public/assets/images/illustrations/celebrating_minion.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /public/assets/images/illustrations/flying_sloth_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toddhgardner/perf-training-website/781e0d80838572d19c124924d61f05c445ea13f8/public/assets/images/illustrations/flying_sloth_2.png -------------------------------------------------------------------------------- /public/assets/images/illustrations/sloth_building_rocket.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /public/assets/images/illustrations/sloth_concerned.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 17 | 18 | 20 | 22 | 24 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 48 | 51 | 57 | 59 | 61 | 63 | 65 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | -------------------------------------------------------------------------------- /public/assets/images/illustrations/sloth_missing_rocket.svg: -------------------------------------------------------------------------------- 1 | not found -------------------------------------------------------------------------------- /public/assets/images/illustrations/sloth_paper_airplane.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /public/assets/images/illustrations/sloth_pricing_rocket.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 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 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | -------------------------------------------------------------------------------- /public/assets/images/illustrations/sloth_rocket_launch.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/assets/images/illustrations/web_performance.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 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 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | -------------------------------------------------------------------------------- /public/assets/images/request_metrics_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toddhgardner/perf-training-website/781e0d80838572d19c124924d61f05c445ea13f8/public/assets/images/request_metrics_logo.png -------------------------------------------------------------------------------- /public/assets/images/request_metrics_logo_dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 17 | 19 | 21 | 23 | 24 | 25 | 27 | 47 | 51 | 53 | 60 | 61 | 65 | 67 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | -------------------------------------------------------------------------------- /public/assets/images/request_metrics_logo_rss.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toddhgardner/perf-training-website/781e0d80838572d19c124924d61f05c445ea13f8/public/assets/images/request_metrics_logo_rss.png -------------------------------------------------------------------------------- /public/assets/images/share.min.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toddhgardner/perf-training-website/781e0d80838572d19c124924d61f05c445ea13f8/public/assets/images/share.min.png -------------------------------------------------------------------------------- /public/assets/images/xmark-circle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /public/assets/images/youtube_avatar_transparent_40x40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toddhgardner/perf-training-website/781e0d80838572d19c124924d61f05c445ea13f8/public/assets/images/youtube_avatar_transparent_40x40.png -------------------------------------------------------------------------------- /public/assets/js/banner.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This renders a Cookie/GDPR consent banner into the page. 3 | */ 4 | (function (ready) { 5 | if (document.readyState === "complete") { 6 | ready(); 7 | } else { 8 | document.addEventListener('readystatechange', function(event) { 9 | if (document.readyState === "complete") { 10 | ready(); 11 | } 12 | }); 13 | } 14 | })(function main() { /* the document is now ready. */ 15 | 16 | var consentBannerEl = document.createElement("div"); 17 | consentBannerEl.classList.add("consent-banner"); 18 | consentBannerEl.innerHTML = "" + 19 | "
" + 20 | "
" + 21 | "

" + 22 | "Privacy Notice
Like every other site on the Internet, we use cookies so that we can measure who visits our page and what they are interested in. We don't know your name or anything else about you. It's okay." + 23 | "

" + 24 | "
" + 25 | "
" + 26 | ""+ 27 | "
" + 28 | "
"; 29 | consentBannerEl.querySelector(".btn").addEventListener("click", function() { 30 | // user accepted 31 | document.body.removeChild(consentBannerEl); 32 | }); 33 | 34 | document.body.insertBefore(consentBannerEl, document.body.children[0]); 35 | 36 | }); -------------------------------------------------------------------------------- /public/assets/js/util/lazyloader.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * lazyLoader 4 | * Check for elements in the document to be loaded later when visible to the user. 5 | * @see https://developers.google.com/web/fundamentals/performance/lazy-loading-guidance/images-and-video/ 6 | * @example 7 | * 8 | */ 9 | (function (ready) { 10 | if (document.readyState === "complete" || document.readyState === "interactive") { 11 | ready(); 12 | } else { 13 | document.addEventListener("DOMContentLoaded", ready); 14 | } 15 | })(function lazyLoader() { /* the document is now ready. */ 16 | 17 | var lazyEls = [].slice.call(document.querySelectorAll("[data-src]")); 18 | 19 | function load(el) { 20 | var src = el.getAttribute("data-src"); 21 | var srcset = el.getAttribute("data-srcset"); 22 | // [NOTE] Todd We shouldn't hit this if data-src was null, but monitoring 23 | // says it happens sometimes, so ¯\_(ツ)_/¯ 24 | if (src) { el.setAttribute("src", src); } 25 | if (srcset) { el.setAttribute("srcset", srcset); } 26 | el.removeAttribute("data-src"); 27 | el.removeAttribute("data-srcset"); 28 | } 29 | 30 | if ("IntersectionObserver" in window) { 31 | var lazyObserver = new IntersectionObserver(function(entries) { 32 | entries.forEach(function(entry) { 33 | if (entry.isIntersecting) { 34 | var el = entry.target; 35 | load(el); 36 | lazyObserver.unobserve(el); 37 | } 38 | }); 39 | }); 40 | 41 | lazyEls.forEach(function(el) { 42 | if (el.tagName === "SCRIPT") { 43 | load(el); 44 | } 45 | else { 46 | lazyObserver.observe(el); 47 | } 48 | }); 49 | } 50 | else { 51 | lazyEls.forEach(load); 52 | } 53 | 54 | }); 55 | -------------------------------------------------------------------------------- /public/assets/js/util/perf.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Custom Real User Monitoring Performance Agent 3 | * Demonstration for "Fundamentals of Web Performance" 4 | * by Todd Gardner 5 | * 6 | * Not for production use. 7 | */ 8 | (() => { 9 | 10 | const payload = { 11 | url: window.location.href, 12 | dcl: 0, 13 | load: 0, 14 | fcp: 0, 15 | lcp: 0, 16 | cls: 0, 17 | fid: 0 18 | } 19 | 20 | // Navigation Performance Timings 21 | onDocumentReady(() => { 22 | setTimeout(() => { // "load" isn't done until the next cycle 23 | let navEntry = performance.getEntriesByType("navigation")[0]; 24 | payload.dcl = navEntry.domContentLoadedEventStart; 25 | payload.load = navEntry.loadEventStart; 26 | console.log('Navigation Performance Timing', navEntry); 27 | }, 0); 28 | }); 29 | 30 | // First Contentful Paint 31 | new PerformanceObserver((entryList) => { 32 | let entries = entryList.getEntries() || []; 33 | entries.forEach((entry) => { 34 | if (entry.name === "first-contentful-paint") { 35 | payload.fcp = entry.startTime; 36 | console.log(`FCP: ${payload.fcp}`); 37 | } 38 | }); 39 | }).observe({ type: "paint", buffered: true }); 40 | 41 | // Largest Contentful Paint 42 | new PerformanceObserver((entryList) => { 43 | let entries = entryList.getEntries() || []; 44 | entries.forEach((entry) => { 45 | if (entry.startTime > payload.lcp) { 46 | payload.lcp = entry.startTime; 47 | console.log(`LCP: ${payload.lcp}`); 48 | } 49 | }); 50 | }).observe({ type: "largest-contentful-paint", buffered: true }); 51 | 52 | // Cumulative Layout Shift 53 | new PerformanceObserver((entryList) => { 54 | let entries = entryList.getEntries() || []; 55 | entries.forEach((entry) => { 56 | if (!entry.hadRecentInput) { 57 | payload.cls += entry.value; 58 | console.log(`CLS: ${payload.cls}`); 59 | } 60 | }); 61 | }).observe({ type: "layout-shift", buffered: true }); 62 | 63 | // First Input Delay 64 | new PerformanceObserver((entryList) => { 65 | let entries = entryList.getEntries() || []; 66 | entries.forEach((entry) => { 67 | payload.fid = entry.processingStart - entry.startTime; 68 | console.log(`FID: ${payload.fid}`); 69 | }); 70 | }).observe({ type: "first-input", buffered: true }); 71 | 72 | 73 | window.addEventListener("visibilitychange", () => { 74 | if (document.visibilityState === 'hidden') { 75 | let data = JSON.stringify(payload); 76 | navigator.sendBeacon("/api/perf", data); 77 | console.log("Sending performance:", data); 78 | } 79 | }); 80 | 81 | })(); 82 | 83 | 84 | 85 | // Utility functions to make example easier to understand. 86 | function onDocumentReady(onReady) { 87 | if (document.readyState === "complete") { onReady(); } 88 | else { 89 | document.addEventListener('readystatechange', (event) => { 90 | if (document.readyState === "complete") { onReady(); } 91 | }); 92 | } 93 | } 94 | 95 | -------------------------------------------------------------------------------- /public/assets/vendor/chatty/avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toddhgardner/perf-training-website/781e0d80838572d19c124924d61f05c445ea13f8/public/assets/vendor/chatty/avatar.png -------------------------------------------------------------------------------- /public/assets/vendor/chatty/chatty.css: -------------------------------------------------------------------------------- 1 | body { 2 | display: flex; 3 | flex-direction: column; 4 | height: calc(100% - 30px); 5 | background-color: #d7efeb; 6 | padding: 12px; 7 | margin: 0; 8 | } 9 | .history { 10 | flex: 1 1 auto; 11 | background-color: white; 12 | border: 2px solid rgb(42, 106, 95); 13 | border-radius: 6px; 14 | padding: 8px; 15 | } 16 | .history .row { 17 | display: flex; 18 | flex-direction: row; 19 | padding: 8px; 20 | opacity: 1; 21 | transition: opacity 100ms linear; 22 | } 23 | .history .row.hide { 24 | opacity: 0; 25 | } 26 | .history .row .head { 27 | flex: 0 0 50px; 28 | } 29 | .history .row .head img { 30 | height: 48px; 31 | width: 48px; 32 | border: 1px solid rgb(42, 106, 95); 33 | border-radius: 24px; 34 | } 35 | .history .row .bubble { 36 | flex: 1 1 auto; 37 | margin: 4px; 38 | padding: 4px; 39 | background-color: rgb(141, 198, 188); 40 | border-radius: 6px; 41 | } 42 | .history .row .bubble .name { 43 | font-weight: bold; 44 | } 45 | 46 | 47 | .control { 48 | flex: 0 0 50px; 49 | display: flex; 50 | flex-direction: row; 51 | padding: 8px 0; 52 | } 53 | .control input { 54 | flex: 1 1 auto; 55 | margin-right: 8px; 56 | font-size: 14px; 57 | } 58 | .control button { 59 | flex: 0 0 50px; 60 | } 61 | 62 | .footer { 63 | flex: 0 0 20px; 64 | text-align: center; 65 | font-size: 12px; 66 | display: flex; 67 | align-items: center; 68 | align-self: center; 69 | } 70 | .footer p { 71 | margin: 0; 72 | } -------------------------------------------------------------------------------- /public/assets/vendor/chatty/chatty.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | if (!window._chatty) { 3 | return; 4 | } 5 | 6 | var config = Object.assign({ open: true }, window._chatty); 7 | 8 | var container = null; 9 | var toggleButton = null; 10 | 11 | function loadChatWidget() { 12 | container = document.createElement('div'); 13 | container.innerHTML = 14 | '' + 15 | "
" + 16 | ""; 17 | container.style.position = 'fixed'; 18 | container.style.bottom = '-600px'; 19 | container.style.right = '10px'; 20 | container.style.maxWidth = '360px'; 21 | container.style.width = '100%'; 22 | container.style.transition = 'all 200ms ease-in-out'; 23 | 24 | var controlBar = container.querySelector('.chatty-control'); 25 | controlBar.style.textAlign = 'right'; 26 | controlBar.style.height = '92px'; 27 | 28 | toggleButton = container.querySelector('#chatty-toggle'); 29 | toggleButton.style.padding = '10px'; 30 | toggleButton.style.margin = '10px'; 31 | toggleButton.style.backgroundColor = '#47b4a1'; 32 | toggleButton.style.color = 'white'; 33 | toggleButton.style.fontWeight = 'bold'; 34 | toggleButton.style.border = '2px solid #2a6a5f'; 35 | toggleButton.style.borderRadius = '72px'; 36 | toggleButton.style.fontSize = '40px'; 37 | toggleButton.style.lineHeight = '50px'; 38 | toggleButton.style.width = '72px'; 39 | 40 | var chattyFrame = container.querySelector('iframe'); 41 | chattyFrame.style.margin = '0 0 -10px 0'; 42 | chattyFrame.style.border = 'none'; 43 | chattyFrame.style.height = '600px'; 44 | chattyFrame.style.width = '100%'; 45 | chattyFrame.style.borderRadius = '6px'; 46 | 47 | toggleButton.addEventListener('click', function () { 48 | if (toggleButton.textContent === 'X') { 49 | close(); 50 | } else { 51 | open(); 52 | } 53 | }); 54 | 55 | document.body.appendChild(container); 56 | 57 | chattyFrame.addEventListener('load', function () { 58 | if (config.open) { 59 | setTimeout(open, 100); 60 | } 61 | }); 62 | } 63 | 64 | function open() { 65 | if (!container) { 66 | return; 67 | } 68 | container.style.bottom = '0'; 69 | toggleButton.textContent = 'X'; 70 | } 71 | 72 | function close() { 73 | if (!container) { 74 | return; 75 | } 76 | container.style.bottom = '-600px'; 77 | toggleButton.textContent = 'Hi'; 78 | } 79 | 80 | (function (ready) { 81 | if ( 82 | document.readyState === 'complete' || 83 | document.readyState === 'interactive' 84 | ) { 85 | ready(); 86 | } else { 87 | document.addEventListener('DOMContentLoaded', ready); 88 | } 89 | })(loadChatWidget); 90 | })(); 91 | -------------------------------------------------------------------------------- /public/assets/vendor/chatty/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 |
9 |
10 |
11 |
Faux Marketson
12 |
13 | Hey there! Can I help you find anything? 14 |
15 |
16 |
17 |
18 | 19 |
20 | 21 | 22 |
23 | 24 | 27 | 28 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /public/assets/vendor/fontello/LICENSE.txt: -------------------------------------------------------------------------------- 1 | Font license info 2 | 3 | 4 | ## Font Awesome 5 | 6 | Copyright (C) 2016 by Dave Gandy 7 | 8 | Author: Dave Gandy 9 | License: SIL () 10 | Homepage: http://fortawesome.github.com/Font-Awesome/ 11 | 12 | 13 | -------------------------------------------------------------------------------- /public/assets/vendor/fontello/README.txt: -------------------------------------------------------------------------------- 1 | This webfont is generated by http://fontello.com open source project. 2 | 3 | 4 | ================================================================================ 5 | Please, note, that you should obey original font licenses, used to make this 6 | webfont pack. Details available in LICENSE.txt file. 7 | 8 | - Usually, it's enough to publish content of LICENSE.txt file somewhere on your 9 | site in "About" section. 10 | 11 | - If your project is open-source, usually, it will be ok to make LICENSE.txt 12 | file publicly available in your repository. 13 | 14 | - Fonts, used in Fontello, don't require a clickable link on your site. 15 | But any kind of additional authors crediting is welcome. 16 | ================================================================================ 17 | 18 | 19 | Comments on archive content 20 | --------------------------- 21 | 22 | - /font/* - fonts in different formats 23 | 24 | - /css/* - different kinds of css, for all situations. Should be ok with 25 | twitter bootstrap. Also, you can skip style and assign icon classes 26 | directly to text elements, if you don't mind about IE7. 27 | 28 | - demo.html - demo file, to show your webfont content 29 | 30 | - LICENSE.txt - license info about source fonts, used to build your one. 31 | 32 | - config.json - keeps your settings. You can import it back into fontello 33 | anytime, to continue your work 34 | 35 | 36 | Why so many CSS files ? 37 | ----------------------- 38 | 39 | Because we like to fit all your needs :) 40 | 41 | - basic file, .css - is usually enough, it contains @font-face 42 | and character code definitions 43 | 44 | - *-ie7.css - if you need IE7 support, but still don't wish to put char codes 45 | directly into html 46 | 47 | - *-codes.css and *-ie7-codes.css - if you like to use your own @font-face 48 | rules, but still wish to benefit from css generation. That can be very 49 | convenient for automated asset build systems. When you need to update font - 50 | no need to manually edit files, just override old version with archive 51 | content. See fontello source code for examples. 52 | 53 | - *-embedded.css - basic css file, but with embedded WOFF font, to avoid 54 | CORS issues in Firefox and IE9+, when fonts are hosted on the separate domain. 55 | We strongly recommend to resolve this issue by `Access-Control-Allow-Origin` 56 | server headers. But if you ok with dirty hack - this file is for you. Note, 57 | that data url moved to separate @font-face to avoid problems with 2 | 3 | 4 | 278 | 279 | 291 | 292 | 293 |
294 |

fontello font demo

295 | 298 |
299 |
300 |
301 |
icon-thumbs-up0xe800
302 |
icon-info-circled0xe801
303 |
icon-rss-squared0xf143
304 |
icon-thumbs-up-alt0xf164
305 |
306 |
307 |
icon-youtube-squared0xf166
308 |
icon-mail-squared0xf199
309 |
icon-hacker-news0xf1d4
310 |
icon-reddit-alien0xf281
311 |
312 |
313 |
icon-twitter-squared0xf304
314 |
icon-facebook-squared0xf308
315 |
icon-linkedin-squared0xf30c
316 |
317 |
318 | 319 | 320 | -------------------------------------------------------------------------------- /public/assets/vendor/fontello/font/fontello.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toddhgardner/perf-training-website/781e0d80838572d19c124924d61f05c445ea13f8/public/assets/vendor/fontello/font/fontello.eot -------------------------------------------------------------------------------- /public/assets/vendor/fontello/font/fontello.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Copyright (C) 2020 by original authors @ fontello.com 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /public/assets/vendor/fontello/font/fontello.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toddhgardner/perf-training-website/781e0d80838572d19c124924d61f05c445ea13f8/public/assets/vendor/fontello/font/fontello.ttf -------------------------------------------------------------------------------- /public/assets/vendor/fontello/font/fontello.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toddhgardner/perf-training-website/781e0d80838572d19c124924d61f05c445ea13f8/public/assets/vendor/fontello/font/fontello.woff -------------------------------------------------------------------------------- /public/assets/vendor/fontello/font/fontello.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toddhgardner/perf-training-website/781e0d80838572d19c124924d61f05c445ea13f8/public/assets/vendor/fontello/font/fontello.woff2 -------------------------------------------------------------------------------- /public/assets/vendor/lite-youtube/lite-youtube.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sources":["jsdelivr-header.js","/npm/@justinribeiro/lite-youtube@0.6.2/lite-youtube.js"],"names":[],"mappings":"AAAA;AACA;AACA;AACA;AACA;AACA;AACA,ACUA,MAAM,oBAAoB,YACxB,cACE,QACA,KAAK,gBAAiB,EACtB,KAAK,aAGP,gCACE,MAAO,CAAC,WAGV,oBACE,KAAK,iBAAiB,cAAe,YAAY,gBAAiB,CAChE,MAAM,IAGR,KAAK,iBAAiB,QAAS,GAAK,KAAK,eAO3C,aACoB,KAAK,aAAa,CAAC,KAAM,SACjC,UAAY,2jFAuFtB,KAAK,cAAgB,KAAK,WAAW,cAAc,UACnD,KAAK,YAAc,CACjB,SAAU,KAAK,WAAW,cAAc,wBACxC,KAAM,KAAK,WAAW,cAAc,oBACpC,KAAM,KAAK,WAAW,cAAc,qBAEtC,KAAK,mBAAqB,KAAK,WAAW,cAAc,gBAO1D,mBACE,KAAK,QAAU,mBAAmB,KAAK,aAAa,YACpD,KAAK,WAAa,KAAK,aAAa,eAAiB,QACrD,KAAK,UAAY,KAAK,aAAa,cAAgB,OACnD,KAAK,aAAe,KAAK,aAAa,UAAY,EAClD,KAAK,SAA6C,KAAlC,KAAK,aAAa,YAElC,KAAK,yBAEL,KAAK,mBAAmB,aACtB,gBACG,KAAK,cAAc,KAAK,cAE7B,KAAK,aAAa,WAAY,KAAK,cAAc,KAAK,cAElD,KAAK,UACP,KAAK,6BAUT,yBAAyB,EAAM,EAAQ,GACrC,OAAQ,GACN,IAAK,UACC,IAAW,IACb,KAAK,mBAGD,KAAK,cAAc,UAAU,SAAS,mBACxC,KAAK,cAAc,UAAU,OAAO,iBACpC,KAAK,WAAW,cAAc,UAAU,YAclD,cACE,IAAK,KAAK,eAAgB,CACxB,MAAM,wKAG2B,KAAK,4BAA4B,KAAK,4BAEvE,KAAK,cAAc,mBAAmB,YAAa,GACnD,KAAK,cAAc,UAAU,IAAI,iBACjC,KAAK,gBAAiB,GAQ1B,yBAEE,YAAY,YAAY,aAAc,wBAEtC,MAAM,iCAA+C,KAAK,yBACpD,4BAA0C,KAAK,wBACrD,KAAK,YAAY,KAAK,OAAS,EAC/B,KAAK,YAAY,KAAK,OAAS,EAC/B,KAAK,YAAY,SAAS,IAAM,EAChC,KAAK,YAAY,SAAS,aACxB,gBACG,KAAK,cAAc,KAAK,cAE7B,KAAK,YAAY,SAAS,aACxB,SACG,KAAK,cAAc,KAAK,cAQ/B,6BACE,GACE,yBAA0B,QAC1B,8BAA+B,OAC/B,CAOiB,IAAI,qBAAqB,CAAC,EAAS,KAClD,EAAQ,QAAQ,IACV,EAAM,iBAAmB,KAAK,iBAChC,YAAY,kBACZ,KAAK,cACL,EAAS,UAAU,UAXT,CACd,KAAM,KACN,WAAY,MACZ,UAAW,IAaJ,QAAQ,OAOrB,mBAAmB,EAAM,EAAK,GAC5B,MAAM,EAAW,SAAS,cAAc,QACxC,EAAS,IAAM,EACf,EAAS,KAAO,EACZ,IACF,EAAS,GAAK,GAEhB,EAAS,aAAc,EACvB,SAAS,KAAK,OAAO,GAavB,yBACM,YAAY,eAEhB,YAAY,YAAY,aAAc,uBAItC,YAAY,YAAY,aAAc,2BAGtC,YAAY,YAAY,aAAc,0BAItC,YAAY,YACV,aACA,uCAEF,YAAY,YAAY,aAAc,kCACtC,YAAY,cAAe,IAI/B,eAAe,OAAO,eAAgB","file":"/npm/@justinribeiro/lite-youtube@0.6.2/lite-youtube.js","sourceRoot":"","sourcesContent":["/**\n * Minified by jsDelivr using Terser v3.14.1.\n * Original file: /npm/@justinribeiro/lite-youtube@0.6.2/lite-youtube.js\n * \n * Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files\n */\n","/**\n *\n * The shadowDom / Intersection Observer version of Paul's concept:\n * https://github.com/paulirish/lite-youtube-embed\n *\n * A lightweight YouTube embed. Still should feel the same to the user, just\n * MUCH faster to initialize and paint.\n *\n * Thx to these as the inspiration\n * https://storage.googleapis.com/amp-vs-non-amp/youtube-lazy.html\n * https://autoplay-youtube-player.glitch.me/\n *\n * Once built it, I also found these (👍👍):\n * https://github.com/ampproject/amphtml/blob/master/extensions/amp-youtube\n * https://github.com/Daugilas/lazyYT https://github.com/vb/lazyframe\n */\nclass LiteYTEmbed extends HTMLElement {\n constructor() {\n super();\n this.__iframeLoaded = false;\n this.__setupDom();\n }\n\n static get observedAttributes() {\n return ['videoid'];\n }\n\n connectedCallback() {\n this.addEventListener('pointerover', LiteYTEmbed.warmConnections, {\n once: true,\n });\n\n this.addEventListener('click', e => this.__addIframe());\n }\n\n /**\n * Define our shadowDOM for the component\n * @private\n */\n __setupDom() {\n const shadowDom = this.attachShadow({mode: 'open'});\n shadowDom.innerHTML = `\n \n
\n \n \n \n \n \n \n
\n `;\n this.__domRefFrame = this.shadowRoot.querySelector('#frame');\n this.__domRefImg = {\n fallback: this.shadowRoot.querySelector('#fallbackPlaceholder'),\n webp: this.shadowRoot.querySelector('#webpPlaceholder'),\n jpeg: this.shadowRoot.querySelector('#jpegPlaceholder'),\n };\n this.__domRefPlayButton = this.shadowRoot.querySelector('.lty-playbtn');\n }\n\n /**\n * Parse our attributes and fire up some placeholders\n * @private\n */\n __setupComponent() {\n this.videoId = encodeURIComponent(this.getAttribute('videoid'));\n this.videoTitle = this.getAttribute('videotitle') || 'Video';\n this.videoPlay = this.getAttribute('videoplay') || 'Play';\n this.videoStartAt = this.getAttribute('start') || 0;\n this.autoLoad = this.getAttribute('autoload') === '' ? true : false;\n\n this.__initImagePlaceholder();\n\n this.__domRefPlayButton.setAttribute(\n 'aria-label',\n `${this.videoPlay}: ${this.videoTitle}`,\n );\n this.setAttribute('title', `${this.videoPlay}: ${this.videoTitle}`);\n\n if (this.autoLoad) {\n this.__initIntersectionObserver();\n }\n }\n\n /**\n * Lifecycle method that we use to listen for attribute changes to period\n * @param {*} name\n * @param {*} oldVal\n * @param {*} newVal\n */\n attributeChangedCallback(name, oldVal, newVal) {\n switch (name) {\n case 'videoid': {\n if (oldVal !== newVal) {\n this.__setupComponent();\n\n // if we have a previous iframe, remove it and the activated class\n if (this.__domRefFrame.classList.contains('lyt-activated')) {\n this.__domRefFrame.classList.remove('lyt-activated');\n this.shadowRoot.querySelector('iframe').remove();\n }\n }\n break;\n }\n default:\n break;\n }\n }\n\n /**\n * Inject the iframe into the component body\n * @private\n */\n __addIframe() {\n if (!this.__iframeLoaded) {\n const iframeHTML = `\n`;\n this.__domRefFrame.insertAdjacentHTML('beforeend', iframeHTML);\n this.__domRefFrame.classList.add('lyt-activated');\n this.__iframeLoaded = true;\n }\n }\n\n /**\n * Setup the placeholder image for the component\n * @private\n */\n __initImagePlaceholder() {\n // we don't know which image type to preload, so warm the connection\n LiteYTEmbed.addPrefetch('preconnect', 'https://i.ytimg.com/');\n\n const posterUrlWebp = `https://i.ytimg.com/vi_webp/${this.videoId}/hqdefault.webp`;\n const posterUrlJpeg = `https://i.ytimg.com/vi/${this.videoId}/hqdefault.jpg`;\n this.__domRefImg.webp.srcset = posterUrlWebp;\n this.__domRefImg.jpeg.srcset = posterUrlJpeg;\n this.__domRefImg.fallback.src = posterUrlJpeg;\n this.__domRefImg.fallback.setAttribute(\n 'aria-label',\n `${this.videoPlay}: ${this.videoTitle}`,\n );\n this.__domRefImg.fallback.setAttribute(\n 'alt',\n `${this.videoPlay}: ${this.videoTitle}`,\n );\n }\n\n /**\n * Setup the Intersection Observer to load the iframe when scrolled into view\n * @private\n */\n __initIntersectionObserver() {\n if (\n 'IntersectionObserver' in window &&\n 'IntersectionObserverEntry' in window\n ) {\n const options = {\n root: null,\n rootMargin: '0px',\n threshold: 0,\n };\n\n const observer = new IntersectionObserver((entries, observer) => {\n entries.forEach(entry => {\n if (entry.isIntersecting && !this.__iframeLoaded) {\n LiteYTEmbed.warmConnections();\n this.__addIframe();\n observer.unobserve(this);\n }\n });\n }, options);\n\n observer.observe(this);\n }\n }\n\n /**\n * Add a to the head\n */\n static addPrefetch(kind, url, as) {\n const linkElem = document.createElement('link');\n linkElem.rel = kind;\n linkElem.href = url;\n if (as) {\n linkElem.as = as;\n }\n linkElem.crossorigin = true;\n document.head.append(linkElem);\n }\n\n /**\n * Begin preconnecting to warm up the iframe load Since the embed's netwok\n * requests load within its iframe, preload/prefetch'ing them outside the\n * iframe will only cause double-downloads. So, the best we can do is warm up\n * a few connections to origins that are in the critical path.\n *\n * Maybe `` would work, but it's unsupported:\n * http://crbug.com/593267 But TBH, I don't think it'll happen soon with Site\n * Isolation and split caches adding serious complexity.\n */\n static warmConnections() {\n if (LiteYTEmbed.preconnected) return;\n // Host that YT uses to serve JS needed by player, per amp-youtube\n LiteYTEmbed.addPrefetch('preconnect', 'https://s.ytimg.com');\n\n // The iframe document and most of its subresources come right off\n // youtube.com\n LiteYTEmbed.addPrefetch('preconnect', 'https://www.youtube.com');\n\n // The botguard script is fetched off from google.com\n LiteYTEmbed.addPrefetch('preconnect', 'https://www.google.com');\n\n // TODO: Not certain if these ad related domains are in the critical path.\n // Could verify with domain-specific throttling.\n LiteYTEmbed.addPrefetch(\n 'preconnect',\n 'https://googleads.g.doubleclick.net',\n );\n LiteYTEmbed.addPrefetch('preconnect', 'https://static.doubleclick.net');\n LiteYTEmbed.preconnected = true;\n }\n}\n// Register custom element\ncustomElements.define('lite-youtube', LiteYTEmbed);\n"]} -------------------------------------------------------------------------------- /public/assets/vendor/lite-youtube/lite-youtube.min.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Minified by jsDelivr using Terser v3.14.1. 3 | * Original file: /npm/@justinribeiro/lite-youtube@0.6.2/lite-youtube.js 4 | * 5 | * Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files 6 | * 7 | * 8 | */ 9 | class LiteYTEmbed extends HTMLElement{constructor(){super(),this.__iframeLoaded=!1,this.__setupDom()}static get observedAttributes(){return["videoid"]}connectedCallback(){this.addEventListener("pointerover",LiteYTEmbed.warmConnections,{once:!0}),this.addEventListener("click",e=>this.__addIframe())}__setupDom(){this.attachShadow({mode:"open"}).innerHTML='\n \n
\n \n \n \n \n \n \n
\n ',this.__domRefFrame=this.shadowRoot.querySelector("#frame"),this.__domRefImg={fallback:this.shadowRoot.querySelector("#fallbackPlaceholder"),webp:this.shadowRoot.querySelector("#webpPlaceholder"),jpeg:this.shadowRoot.querySelector("#jpegPlaceholder")},this.__domRefPlayButton=this.shadowRoot.querySelector(".lty-playbtn")}__setupComponent(){this.videoId=encodeURIComponent(this.getAttribute("videoid")),this.videoTitle=this.getAttribute("videotitle")||"Video",this.videoPlay=this.getAttribute("videoplay")||"Play",this.videoStartAt=this.getAttribute("start")||0,this.autoLoad=""===this.getAttribute("autoload"),this.__initImagePlaceholder(),this.__domRefPlayButton.setAttribute("aria-label",`${this.videoPlay}: ${this.videoTitle}`),this.setAttribute("title",`${this.videoPlay}: ${this.videoTitle}`),this.autoLoad&&this.__initIntersectionObserver()}attributeChangedCallback(e,t,i){switch(e){case"videoid":t!==i&&(this.__setupComponent(),this.__domRefFrame.classList.contains("lyt-activated")&&(this.__domRefFrame.classList.remove("lyt-activated"),this.shadowRoot.querySelector("iframe").remove()))}}__addIframe(){if(!this.__iframeLoaded){const e=`\n`;this.__domRefFrame.insertAdjacentHTML("beforeend",e),this.__domRefFrame.classList.add("lyt-activated"),this.__iframeLoaded=!0}}__initImagePlaceholder(){LiteYTEmbed.addPrefetch("preconnect","https://i.ytimg.com/");const e=`https://i.ytimg.com/vi_webp/${this.videoId}/hqdefault.webp`,t=`https://i.ytimg.com/vi/${this.videoId}/hqdefault.jpg`;this.__domRefImg.webp.srcset=e,this.__domRefImg.jpeg.srcset=t,this.__domRefImg.fallback.src=t,this.__domRefImg.fallback.setAttribute("aria-label",`${this.videoPlay}: ${this.videoTitle}`),this.__domRefImg.fallback.setAttribute("alt",`${this.videoPlay}: ${this.videoTitle}`)}__initIntersectionObserver(){if("IntersectionObserver"in window&&"IntersectionObserverEntry"in window){new IntersectionObserver((e,t)=>{e.forEach(e=>{e.isIntersecting&&!this.__iframeLoaded&&(LiteYTEmbed.warmConnections(),this.__addIframe(),t.unobserve(this))})},{root:null,rootMargin:"0px",threshold:0}).observe(this)}}static addPrefetch(e,t,i){const n=document.createElement("link");n.rel=e,n.href=t,i&&(n.as=i),n.crossorigin=!0,document.head.append(n)}static warmConnections(){LiteYTEmbed.preconnected||(LiteYTEmbed.addPrefetch("preconnect","https://s.ytimg.com"),LiteYTEmbed.addPrefetch("preconnect","https://www.youtube.com"),LiteYTEmbed.addPrefetch("preconnect","https://www.google.com"),LiteYTEmbed.addPrefetch("preconnect","https://googleads.g.doubleclick.net"),LiteYTEmbed.addPrefetch("preconnect","https://static.doubleclick.net"),LiteYTEmbed.preconnected=!0)}}customElements.define("lite-youtube",LiteYTEmbed); 10 | //# sourceMappingURL=lite-youtube.map -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toddhgardner/perf-training-website/781e0d80838572d19c124924d61f05c445ea13f8/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Request Metrics | Unified Analytics and Client-Side Observability. Get easy metrics and actionable alerts to 11 | make 12 | your users happy and system better. 13 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 24 | 26 | 27 | 28 | 29 | 30 | 44 | 45 | 46 | 47 | 48 | 79 |
80 |
81 |
82 | Sloth riding a rocket 83 |
84 | 85 |
86 |
87 |

Unified Analytics

88 |

89 | Request Metrics monitors your software as real users see it–everything else is just diagnostics. 90 | Get simplified visibility into your traffic, users, behavior, performance, and APIs, all with a single 91 | script tag. 92 |

93 |
94 |
95 | 96 | Start Your Free Trial 97 | 98 | No credit card required 99 |
100 | 101 | See Live Demo 102 | 103 |
104 |
105 |
106 |
107 | 108 |
109 |
110 |

How It Works

111 |
112 |
113 | 116 |
117 |
118 |
119 |
120 | 121 |
122 |
123 | 124 |
125 |

Our Features

126 |
    127 |
  • 128 | checkmark 129 | User Session Analytics 130 |
  • 131 |
  • 132 | checkmark 133 | Web Performance 134 |
  • 135 |
  • 136 | checkmark 137 | API Monitoring 138 |
  • 139 |
  • 140 | checkmark 141 | Error Reporting 142 |
  • 143 |
144 |
145 | 146 |
147 |

Their Features

148 |
    149 |
  • 150 | No checkmark 151 | Fake "Synthetic" Performance 152 |
  • 153 |
  • 154 | No checkmark 155 | Complex Query-Based Reporting 156 |
  • 157 |
  • 158 | No checkmark 159 | Information Overload 160 |
  • 161 |
  • 162 | No checkmark 163 | Unknowable Metered Pricing 164 |
  • 165 |
166 |
167 | 168 |
169 |
170 | 171 |
172 |
173 | 174 |
175 |
176 | 177 | Chart page views and the distribution user load time 179 | 180 |
181 |
182 |

Uncover Real User Experience

183 |

184 | Quickly understand how real users experience your website's performance 185 | for each page and API endpoint. Load Time Distribution shows you how many of 186 | your users are frustrated with slow performance. 187 |

188 |
189 |
190 | 191 |
192 |
193 | 194 | Chart page views and the distribution user load time 196 | 197 |
198 |
199 |

See What's Making Your Pages Slow

200 |

201 | Easily see what is slowing down your users. Page Load Breakdown 202 | shows you where to find opportunities to improve performance. 203 |

204 |
205 |
206 | 207 |
208 |
209 | 210 | Chart page views and the distribution user load time 212 | 213 |
214 |
215 |

Web Vital Performance Metrics

216 |

217 | Get the whole performance picture by understanding your 218 | Core Web Vitals. 219 | See when your user's experience delayed rendering, jank, or 220 | slow interactions. 221 |

222 |
223 |
224 | 225 |
226 |
227 | 228 | 229 |
230 |
231 |
232 | 233 |
234 |

Protected User Privacy

235 |

236 | Request Metrics does not place cookies, store data, or capture anything 237 | identifiable about your website visitors. We aggregate data across useful metrics 238 | to give you the visibility you need, without sacrificing user privacy. 239 |

240 |

241 | Learn more about user privacy. 242 |

243 |
244 | 245 |
246 | Sloth concerned. 247 |
248 | 249 |
250 |
251 |
252 | 253 |
254 |
255 | 256 |
257 | 258 |
259 |

Your Website, Faster

260 | Sloth building a rocket 261 |
262 | 263 |
264 |
265 |

Monitor Performance From Real Users

266 |

267 | We gather metrics from real users visiting your website and 268 | aggregate their experience to show you the performance that really matters. 269 |

270 |
271 |
272 |

Measure Your Performance Over Time

273 |

274 | We rollup your performance statistics to minutes, hours, and days so you 275 | can understand how you compare to last week, last month, or last year. 276 |

277 |
278 |
279 |

Know When Your Website Slows Down

280 |

281 | When something is slowing down and your users are frustrated, we'll let you know 282 | with a detailed alert report about what's happening and the impact. 283 |

284 |
285 |
286 |

Worth The Price

287 |

288 | No suite of tools to deploy, training to attend, or budget approvals. We show you 289 | the most important performance metrics you need, simplified, at a price that fits 290 | any team's budget. 291 |

292 |
293 |
294 | 295 |
296 | 297 |
298 |
299 | 300 |
301 |
302 |
303 | 304 |
305 |

Only $10/month

306 |

for up to 50,000 page views per month

307 | 308 | 313 |
314 | 315 |
316 |
317 |
318 | 319 |
320 |
321 | 322 |

Check Out A Live Demo

323 |

324 | You'll have to see this to believe it. View the live performance metrics for our own 325 | websites in Request Metrics. 326 |

327 | 332 | 333 |
334 |
335 | 336 |
337 | 385 | 386 | 387 | 388 | -------------------------------------------------------------------------------- /server/server.js: -------------------------------------------------------------------------------- 1 | /** 2 | * server.js 3 | * Webserver simulation. You shouldn't need to modify this as part of the 4 | * training 5 | */ 6 | const fs = require("fs"); 7 | const path = require("path"); 8 | 9 | const express = require("express"); 10 | const bodyParser = require("body-parser"); 11 | const compression = require("compression"); 12 | 13 | const config = JSON.parse(fs.readFileSync("package.json")).config; 14 | 15 | const publicDir = path.resolve(__dirname, "../public"); 16 | const logDir = path.resolve(__dirname, "../logs"); 17 | const logFile = path.resolve(logDir, "perf.log.csv"); 18 | 19 | // 20 | // Setup logfile 21 | if (!fs.existsSync(logDir)) { 22 | fs.mkdirSync(logDir, { recursive: true }); 23 | } 24 | if (!fs.existsSync(logFile)) { 25 | fs.writeFileSync(logFile, "time,url,dcl,load,fcp,lcp,cls,fid\n", { flag: "wx" }); 26 | } 27 | 28 | 29 | // 30 | // Server Basic Setup 31 | const server = express(); 32 | if (config["server-compress"]) { 33 | server.use(compression({ filter: () => true })); 34 | } 35 | server.use((req, res, next) => { 36 | setTimeout(next, config["server-delay"]); 37 | }); 38 | 39 | 40 | // 41 | // Disable Asset Caching 42 | server.use((req, res, next) => { 43 | res.setHeader("Cache-Control", "no-store"); 44 | next(); 45 | }); 46 | 47 | 48 | // 49 | // Performance API 50 | server.post("/api/perf", bodyParser.json({ type: "*/*" }), (req, res, next) => { 51 | const now = new Date().getTime() / 1000; 52 | const record = `${now},${req.body.url},${req.body.dcl},${req.body.load},${req.body.fcp},${req.body.lcp},${req.body.cls},${req.body.fid}`; 53 | console.log(record); 54 | 55 | fs.appendFile(logFile, `${record}\n`, (err) => { 56 | if (err) { 57 | console.error(err); 58 | res.sendStatus(500); 59 | } 60 | else { 61 | res.sendStatus(200); 62 | } 63 | next(); 64 | }); 65 | 66 | }); 67 | 68 | 69 | // 70 | // Public file hosting 71 | server.use(express.static(publicDir, { etag: false })); 72 | 73 | 74 | // 75 | // Start Server 76 | const port = parseInt(config["server-port"], 10); 77 | server.listen(port, () => { 78 | console.log(`Server is listening on http://localhost:${port}/`); 79 | }); 80 | server.listen(port+1, () => { 81 | console.log(`Server is listening on http://localhost:${port+1}/`); 82 | }); -------------------------------------------------------------------------------- /slide_title.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toddhgardner/perf-training-website/781e0d80838572d19c124924d61f05c445ea13f8/slide_title.png -------------------------------------------------------------------------------- /tools/imageOptimizer.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const fs = require('fs').promises; 3 | 4 | const imagemin = require('imagemin'); 5 | const imageminJpegtran = require('imagemin-jpegtran'); 6 | const imageminPngquant = require('imagemin-pngquant'); 7 | const imageminSvgo = require('imagemin-svgo'); 8 | const imageminGifsicle = require('imagemin-gifsicle'); 9 | 10 | (async () => { 11 | const files = await imagemin(['public/assets/images/**/!(*.min).{jpg,png,svg,gif}'], { 12 | plugins: [ 13 | imageminJpegtran(), 14 | imageminPngquant({ 15 | strip: true, 16 | quality: [0.2, 0.6] 17 | }), 18 | imageminSvgo({ 19 | plugins: [ 20 | {removeViewBox: false} 21 | ] 22 | }), 23 | imageminGifsicle() 24 | ] 25 | }).then((files) => { 26 | files.forEach(async (file) => { 27 | const source = path.parse(file.sourcePath); 28 | file.destinationPath = `${source.dir}/${source.name}.min${source.ext}`; 29 | await fs.writeFile(file.destinationPath, file.data); 30 | }); 31 | }); 32 | })(); --------------------------------------------------------------------------------