├── .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 | "I Understand "+
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 | "Hi
" +
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 | Say
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 |
299 |
300 |
301 |
icon-thumbs-up 0xe800
302 |
icon-info-circled 0xe801
303 |
icon-rss-squared 0xf143
304 |
icon-thumbs-up-alt 0xf164
305 |
306 |
307 |
icon-youtube-squared 0xf166
308 |
icon-mail-squared 0xf199
309 |
icon-hacker-news 0xf1d4
310 |
icon-reddit-alien 0xf281
311 |
312 |
313 |
icon-twitter-squared 0xf304
314 |
icon-facebook-squared 0xf308
315 |
icon-linkedin-squared 0xf30c
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 = `\nVIDEO `;\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=`\nVIDEO `;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 |
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 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
How It Works
111 |
112 |
113 | VIDEO
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
Our Features
126 |
127 |
128 |
129 | User Session Analytics
130 |
131 |
132 |
133 | Web Performance
134 |
135 |
136 |
137 | API Monitoring
138 |
139 |
140 |
141 | Error Reporting
142 |
143 |
144 |
145 |
146 |
147 |
Their Features
148 |
149 |
150 |
151 | Fake "Synthetic" Performance
152 |
153 |
154 |
155 | Complex Query-Based Reporting
156 |
157 |
158 |
159 | Information Overload
160 |
161 |
162 |
163 | Unknowable Metered Pricing
164 |
165 |
166 |
167 |
168 |
169 |
170 |
171 |
172 |
173 |
174 |
175 |
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 |
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 |
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 |
247 |
248 |
249 |
250 |
251 |
252 |
253 |
254 |
255 |
256 |
257 |
258 |
259 |
Your Website, Faster
260 |
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 | })();
--------------------------------------------------------------------------------