├── .prettierrc
├── screenshot.png
├── src
├── index.js
├── index-styled.js
├── styles.css
└── web-vitals.js
├── rollup.config.js
├── LICENSE
├── index.html
├── package.json
├── README.md
├── .gitignore
└── CODE-OF-CONDUCT.md
/.prettierrc:
--------------------------------------------------------------------------------
1 | { "singleQuote": true }
2 |
--------------------------------------------------------------------------------
/screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stefanjudis/web-vitals-element/HEAD/screenshot.png
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import WebVitals from './web-vitals.js';
2 |
3 | customElements.define('web-vitals', WebVitals);
4 |
--------------------------------------------------------------------------------
/src/index-styled.js:
--------------------------------------------------------------------------------
1 | import WebVitals from './web-vitals.js';
2 | import styles from './styles.css';
3 |
4 | customElements.define('web-vitals', WebVitals);
5 |
--------------------------------------------------------------------------------
/rollup.config.js:
--------------------------------------------------------------------------------
1 | import { nodeResolve } from '@rollup/plugin-node-resolve';
2 | import { terser } from 'rollup-plugin-terser';
3 | import postcss from 'rollup-plugin-postcss';
4 |
5 | const IS_PRODUCTION = process.env.NODE_ENV === 'production';
6 |
7 | export default [
8 | {
9 | input: 'src/index.js',
10 | output: {
11 | format: 'es',
12 | file: IS_PRODUCTION
13 | ? 'dist/web-vitals-element.min.js'
14 | : 'dist/web-vitals-element.js',
15 | },
16 | plugins: [
17 | nodeResolve(),
18 | IS_PRODUCTION ? terser() : null,
19 | postcss({
20 | plugins: [],
21 | }),
22 | ],
23 | },
24 | {
25 | input: 'src/index-styled.js',
26 | output: {
27 | format: 'es',
28 | file: IS_PRODUCTION
29 | ? 'dist/web-vitals-element.styled.min.js'
30 | : 'dist/web-vitals-element.styled.js',
31 | },
32 | plugins: [
33 | nodeResolve(),
34 | IS_PRODUCTION ? terser() : null,
35 | postcss({
36 | plugins: [],
37 | }),
38 | ],
39 | },
40 | ];
41 |
--------------------------------------------------------------------------------
/src/styles.css:
--------------------------------------------------------------------------------
1 | .web-vitals {
2 | max-width: 20em;
3 | border-radius: 0.5em;
4 | overflow: hidden;
5 | padding: 1em;
6 | font-family: Arial, Helvetica, sans-serif;
7 | box-shadow: 0 0.25em 0.35em rgba(23, 23, 23, 0.4);
8 | color: #444;
9 | background: #fff;
10 | }
11 |
12 | .web-vitals a {
13 | color: #444;
14 | font-weight: bold;
15 | }
16 |
17 | .web-vitals dl {
18 | margin: 0;
19 | grid-template-columns: 1fr 1fr;
20 | }
21 |
22 | .web-vitals p {
23 | margin: 0.5em 0 0;
24 | font-size: 0.75em;
25 | }
26 |
27 | .web-vitals div {
28 | display: grid;
29 | grid-template-columns: 3fr 1fr;
30 | }
31 |
32 | .web-vitals div dd::after {
33 | margin-left: 0.25em;
34 | line-height: 1;
35 | }
36 |
37 | .web-vitals div:not(.is-done) dd::after {
38 | content: '🕐';
39 | }
40 |
41 | .web-vitals div.is-good dd::after {
42 | content: '✅';
43 | }
44 |
45 | .web-vitals div.needs-improvement dd::after {
46 | content: '⚠️';
47 | }
48 |
49 | .web-vitals div.is-poor dd::after {
50 | content: '❌';
51 | }
52 |
53 | .web-vitals dd {
54 | text-align: right;
55 | margin-left: 1em;
56 | }
57 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Stefan Judis
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 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | web-vitals-element
6 |
7 |
8 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 | web vitals custom element
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "web-vitals-element",
3 | "version": "1.3.6",
4 | "description": "",
5 | "main": "dist/web-vitals-element.js",
6 | "exports": {
7 | ".": {
8 | "import": "./dist/web-vitals-element.min.js"
9 | },
10 | "./styled": {
11 | "import": "./dist/web-vitals-element.styled.min.js"
12 | }
13 | },
14 | "scripts": {
15 | "dev:server": "live-server .",
16 | "dev:bundle": "rollup -cw",
17 | "dev": "run-p dev:*",
18 | "bundle": "NODE_ENV=production rollup -c",
19 | "prepublish": "NODE_ENV=production rollup -c && rollup -c"
20 | },
21 | "type": "module",
22 | "files": [
23 | "dist/**"
24 | ],
25 | "repository": {
26 | "type": "git",
27 | "url": "git+https://github.com/stefanjudis/web-vitals-element.git"
28 | },
29 | "keywords": [
30 | "web-vitals",
31 | "performance",
32 | "metrics",
33 | "CLS",
34 | "FCP",
35 | "FID",
36 | "LCP",
37 | "TTFB"
38 | ],
39 | "author": "stefan judis ",
40 | "license": "MIT",
41 | "bugs": {
42 | "url": "https://github.com/stefanjudis/web-vitals-element/issues"
43 | },
44 | "homepage": "https://github.com/stefanjudis/web-vitals-element#readme",
45 | "devDependencies": {
46 | "@rollup/plugin-node-resolve": "^13.0.0",
47 | "live-server": "^1.2.1",
48 | "npm-run-all": "^4.1.5",
49 | "postcss": "^8.3.5",
50 | "rollup": "^2.52.3",
51 | "rollup-plugin-postcss": "^4.0.0",
52 | "rollup-plugin-terser": "^7.0.2"
53 | },
54 | "dependencies": {
55 | "web-vitals": "^2.0.1"
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # web-vitals-element
2 |
3 | > Bring [web vitals](https://github.com/GoogleChrome/web-vitals) quickly into your page using custom elements
4 |
5 | 
6 |
7 | [See it in action on CodePen](https://codepen.io/stefanjudis/pen/wvGzvWx).
8 |
9 | ## Basic usage
10 |
11 | ```html
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
31 | ```
32 |
33 | _The element does not render shadow DOM. You can style it like any other element in your HTML page._
34 |
35 | After loading the element script, use the `web-vitals` element in your HTML.
36 |
37 | ```html
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 | ```
47 |
48 | Currently supported metrics: `cls`, `fcp`, `fid`, `lcp`, `ttfb`. Read more about these in [the web-vitals documentation](https://github.com/GoogleChrome/web-vitals).
49 |
50 | ## Contributing
51 |
52 | _I'd love to see more themes for the web vitals element box – the fancier the better!_ If you're interested in contributing some fancy looks, please [open an issue](https://github.com/stefanjudis/web-vitals-element/issues/new).
53 |
54 | ## Code of conduct
55 |
56 | This project underlies [a code of conduct](./CODE-OF-CONDUCT.md).
57 |
58 | ## License
59 |
60 | This project is released under [MIT license](./LICENSE).
61 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | lerna-debug.log*
8 |
9 | # Diagnostic reports (https://nodejs.org/api/report.html)
10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
11 |
12 | # Runtime data
13 | pids
14 | *.pid
15 | *.seed
16 | *.pid.lock
17 |
18 | # Directory for instrumented libs generated by jscoverage/JSCover
19 | lib-cov
20 |
21 | # Coverage directory used by tools like istanbul
22 | coverage
23 | *.lcov
24 |
25 | # nyc test coverage
26 | .nyc_output
27 |
28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
29 | .grunt
30 |
31 | # Bower dependency directory (https://bower.io/)
32 | bower_components
33 |
34 | # node-waf configuration
35 | .lock-wscript
36 |
37 | # Compiled binary addons (https://nodejs.org/api/addons.html)
38 | build/Release
39 |
40 | # Dependency directories
41 | node_modules/
42 | jspm_packages/
43 |
44 | # Snowpack dependency directory (https://snowpack.dev/)
45 | web_modules/
46 |
47 | # TypeScript cache
48 | *.tsbuildinfo
49 |
50 | # Optional npm cache directory
51 | .npm
52 |
53 | # Optional eslint cache
54 | .eslintcache
55 |
56 | # Microbundle cache
57 | .rpt2_cache/
58 | .rts2_cache_cjs/
59 | .rts2_cache_es/
60 | .rts2_cache_umd/
61 |
62 | # Optional REPL history
63 | .node_repl_history
64 |
65 | # Output of 'npm pack'
66 | *.tgz
67 |
68 | # Yarn Integrity file
69 | .yarn-integrity
70 |
71 | # dotenv environment variables file
72 | .env
73 | .env.test
74 |
75 | # parcel-bundler cache (https://parceljs.org/)
76 | .cache
77 | .parcel-cache
78 |
79 | # Next.js build output
80 | .next
81 | out
82 |
83 | # Nuxt.js build / generate output
84 | .nuxt
85 | dist
86 |
87 | # Gatsby files
88 | .cache/
89 | # Comment in the public line in if your project uses Gatsby and not Next.js
90 | # https://nextjs.org/blog/next-9-1#public-directory-support
91 | # public
92 |
93 | # vuepress build output
94 | .vuepress/dist
95 |
96 | # Serverless directories
97 | .serverless/
98 |
99 | # FuseBox cache
100 | .fusebox/
101 |
102 | # DynamoDB Local files
103 | .dynamodb/
104 |
105 | # TernJS port file
106 | .tern-port
107 |
108 | # Stores VSCode versions used for testing VSCode extensions
109 | .vscode-test
110 |
111 | # yarn v2
112 | .yarn/cache
113 | .yarn/unplugged
114 | .yarn/build-state.yml
115 | .yarn/install-state.gz
116 | .pnp.*
117 |
118 | dist
119 |
--------------------------------------------------------------------------------
/CODE-OF-CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | In the interest of fostering an open and welcoming environment, we as
6 | contributors and maintainers pledge to making participation in our project and
7 | our community a harassment-free experience for everyone, regardless of age, body
8 | size, disability, ethnicity, gender identity and expression, level of experience,
9 | nationality, personal appearance, race, religion, or sexual identity and
10 | orientation.
11 |
12 | ## Our Standards
13 |
14 | Examples of behavior that contributes to creating a positive environment
15 | include:
16 |
17 | - Using welcoming and inclusive language
18 | - Being respectful of differing viewpoints and experiences
19 | - Gracefully accepting constructive criticism
20 | - Focusing on what is best for the community
21 | - Showing empathy towards other community members
22 |
23 | Examples of unacceptable behavior by participants include:
24 |
25 | - The use of sexualized language or imagery and unwelcome sexual attention or
26 | advances
27 | - Trolling, insulting/derogatory comments, and personal or political attacks
28 | - Public or private harassment
29 | - Publishing others' private information, such as a physical or electronic
30 | address, without explicit permission
31 | - Other conduct which could reasonably be considered inappropriate in a
32 | professional setting
33 |
34 | ## Our Responsibilities
35 |
36 | Project maintainers are responsible for clarifying the standards of acceptable
37 | behavior and are expected to take appropriate and fair corrective action in
38 | response to any instances of unacceptable behavior.
39 |
40 | Project maintainers have the right and responsibility to remove, edit, or
41 | reject comments, commits, code, wiki edits, issues, and other contributions
42 | that are not aligned to this Code of Conduct, or to ban temporarily or
43 | permanently any contributor for other behaviors that they deem inappropriate,
44 | threatening, offensive, or harmful.
45 |
46 | ## Scope
47 |
48 | This Code of Conduct applies both within project spaces and in public spaces
49 | when an individual is representing the project or its community. Examples of
50 | representing a project or community include using an official project e-mail
51 | address, posting via an official social media account, or acting as an appointed
52 | representative at an online or offline event. Representation of a project may be
53 | further defined and clarified by project maintainers.
54 |
55 | ## Enforcement
56 |
57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
58 | reported by contacting the project team at stefanjudis@gmail.com. All
59 | complaints will be reviewed and investigated and will result in a response that
60 | is deemed necessary and appropriate to the circumstances. The project team is
61 | obligated to maintain confidentiality with regard to the reporter of an incident.
62 | Further details of specific enforcement policies may be posted separately.
63 |
64 | Project maintainers who do not follow or enforce the Code of Conduct in good
65 | faith may face temporary or permanent repercussions as determined by other
66 | members of the project's leadership.
67 |
68 | ## Attribution
69 |
70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71 | available at [https://contributor-covenant.org/version/1/4][version]
72 |
73 | [homepage]: https://contributor-covenant.org
74 | [version]: https://contributor-covenant.org/version/1/4/
75 |
--------------------------------------------------------------------------------
/src/web-vitals.js:
--------------------------------------------------------------------------------
1 | import * as webVitals from 'web-vitals';
2 |
3 | const MS_UNIT = 'ms';
4 |
5 | // borrowed from the vitals extension
6 | // https://github.com/GoogleChrome/web-vitals-extension/blob/master/src/browser_action/vitals.js#L20-L23
7 | const METRIC_CONFIG = new Map([
8 | [
9 | 'CLS',
10 | {
11 | thresholds: {
12 | good: 0.1,
13 | needsImprovement: 0.25,
14 | },
15 | observerEntryType: 'layout-shift',
16 | explainerURL: 'https://web.dev/cls/',
17 | longName: 'Cumulative Layout Shift',
18 | roundFn: (value) => Math.floor(value * 100) / 100,
19 | },
20 | ],
21 | [
22 | 'FCP',
23 | {
24 | thresholds: {
25 | good: 2500,
26 | },
27 | observerEntryType: 'paint',
28 | explainerURL: 'https://web.dev/fcp/',
29 | unit: MS_UNIT,
30 | longName: 'First Contentful Paint',
31 | },
32 | ],
33 | [
34 | 'FID',
35 | {
36 | thresholds: {
37 | good: 100,
38 | needsImprovement: 300,
39 | },
40 | observerEntryType: 'first-input',
41 | explainerURL: 'https://web.dev/fid/',
42 | unit: MS_UNIT,
43 | longName: 'First Input Delay',
44 | },
45 | ],
46 | [
47 | 'LCP',
48 | {
49 | thresholds: {
50 | good: 2500,
51 | needsImprovement: 4000,
52 | },
53 | observerEntryType: 'paint',
54 | explainerURL: 'https://web.dev/lcp/',
55 | unit: MS_UNIT,
56 | longName: 'Largest Contentful Paint',
57 | },
58 | ],
59 | [
60 | 'TTFB',
61 | {
62 | thresholds: {
63 | good: 2500,
64 | },
65 | explainerURL: 'https://web.dev/ttfb/',
66 | unit: MS_UNIT,
67 | longName: 'Time to First Byte',
68 | },
69 | ],
70 | ]);
71 |
72 | const GENERAL_ATTRIBUTES = ['class', 'style'];
73 | const CONFIG_ATTRIBUTES = ['show-unsupported', 'show-metric-name'];
74 |
75 | class WebVitals extends HTMLElement {
76 | constructor() {
77 | super();
78 |
79 | this.unsupportedMetrics = [];
80 | this.metrics = new Map();
81 | }
82 |
83 | connectedCallback() {
84 | const metricAttributes = this.getMetricAttributes();
85 | const metricList = metricAttributes.length
86 | ? metricAttributes
87 | : [...METRIC_CONFIG.keys()];
88 |
89 | this.metrics = this.getMetrics(metricList);
90 |
91 | this.render();
92 |
93 | for (let metricConfig of this.metrics.values()) {
94 | const { name, getWebVitalsValue } = metricConfig;
95 |
96 | getWebVitalsValue((metric) => {
97 | this.metrics.set(name, {
98 | ...metricConfig,
99 | ...metric,
100 | });
101 | this.render();
102 | }, true);
103 | }
104 | }
105 |
106 | getMetricAttributes() {
107 | return this.getAttributeNames()
108 | .filter(
109 | (attr) =>
110 | !GENERAL_ATTRIBUTES.includes(attr) &&
111 | !CONFIG_ATTRIBUTES.includes(attr)
112 | )
113 | .map((attr) => attr.toUpperCase());
114 | }
115 |
116 | getMetrics(metricList) {
117 | return new Map(
118 | metricList.reduce((acc, metricName) => {
119 | // exclude metric when it's not supported by web-vitals
120 | const getWebVitalsValue = webVitals[`get${metricName}`];
121 | if (!getWebVitalsValue) {
122 | console.error(`${metricName} is not supported by ''`);
123 | this.unsupportedMetrics.push(metricName);
124 | return acc;
125 | }
126 |
127 | // exclude metric when it's not supported
128 | const metricConfig = METRIC_CONFIG.get(metricName);
129 | const { observerEntryType } = metricConfig;
130 | if (
131 | observerEntryType &&
132 | !PerformanceObserver.supportedEntryTypes.includes(observerEntryType)
133 | ) {
134 | console.error(`${metricName} is not supported by your browser`);
135 | this.unsupportedMetrics.push(metricName);
136 | return acc;
137 | }
138 |
139 | return [
140 | ...acc,
141 | [
142 | metricName,
143 | {
144 | ...METRIC_CONFIG.get(metricName),
145 | getWebVitalsValue,
146 | name: metricName,
147 | },
148 | ],
149 | ];
150 | }, [])
151 | );
152 | }
153 |
154 | render() {
155 | this.innerHTML = `
156 |
157 | ${[...this.metrics]
158 | .map(([key, metric]) => {
159 | const { explainerURL, longName, roundFn, thresholds, unit, value } =
160 | metric;
161 | let classes = '';
162 | const roundValue = roundFn || Math.floor;
163 | const { good, needsImprovement } = thresholds;
164 |
165 | if (value) {
166 | classes += 'is-final ';
167 | let score = 'is-poor';
168 | if (needsImprovement && value <= needsImprovement) {
169 | score = 'needs-improvement';
170 | }
171 | if (value <= good) {
172 | score = 'is-good';
173 | }
174 | classes += score;
175 | }
176 |
177 | return `
178 |
179 |
-
180 | ${
181 | this.hasAttribute('show-metric-name')
182 | ? `${longName} (${key})`
183 | : `${key}`
184 | }
185 |
186 |
- ${
187 | value ? `${roundValue(value)}${unit ? unit : ''}` : '...'
188 | }
189 |
190 | `;
191 | })
192 | .join('')}
193 |
194 | ${
195 | this.unsupportedMetrics.length &&
196 | this.hasAttribute('show-unsupported')
197 | ? `
Not supported: ${this.unsupportedMetrics.join(', ')}
`
198 | : ''
199 | }
200 |
`;
201 | }
202 | }
203 |
204 | export default WebVitals;
205 |
--------------------------------------------------------------------------------