├── .github └── workflows │ └── update-readme.yml ├── .gitignore ├── Readme.md ├── chromium ├── bin.ts ├── bookmarks │ ├── constants.ts │ ├── index.ts │ ├── types.ts │ └── utils.ts ├── constants.ts ├── index.ts ├── package.json └── tsconfig.json ├── docs ├── autohotkey.md ├── constants.ts ├── how-to-use-it-with-bookmarks.md ├── how-to-use-it-with-chromium.md ├── how-to-use-it-with-console-tab.md ├── how-to-use-it-with-sources-tab.md ├── images │ ├── autohotkey-example.png │ └── bookmarks-edit.png ├── index.ts ├── quick-script-click.ahk └── tsconfig.json ├── package-lock.json ├── package.json ├── snippets ├── _SNIPPET_TEMPLATE │ ├── Readme.md │ └── index.ts ├── bin.ts ├── check-first-and-third-party-script-timings │ ├── Readme.md │ └── index.js ├── check-first-and-third-party-script │ ├── Readme.md │ └── index.ts ├── check-header │ ├── Readme.md │ └── index.js ├── check-image-sizes │ ├── Readme.md │ └── index.js ├── check-image-srcset │ ├── Readme.md │ ├── assets │ │ ├── srcset-script-debugging--console_michael-hladky.png │ │ ├── srcset-script-debugging--oversize_michael-hladky.png │ │ ├── srcset-script-debugging--undersize_michael-hladky.png │ │ ├── srcset-script-debugging__michael-hladky.gif │ │ └── srcset-script-debugging__michael-hladky.mp4 │ └── index.ts ├── check-image-usage │ ├── Readme.md │ └── index.js ├── cls │ ├── Readme.md │ └── index.js ├── constants.ts ├── cumulative-layout-shift │ ├── Readme.md │ └── index.js ├── full-relayout │ ├── Readme.md │ ├── images │ │ ├── full-page-relayout-comparison.png │ │ └── full-page-relayout.png │ └── index.js ├── gathering-styles │ ├── Readme.md │ └── index.js ├── getAttributeDirectives │ ├── Readme.md │ └── index.js ├── getComponentsNodes │ ├── Readme.md │ └── index.js ├── getDOMEventListeners │ ├── Readme.md │ └── index.js ├── getNodesInfo │ ├── Readme.md │ └── index.js ├── index.ts ├── lcp │ ├── Readme.md │ └── index.js ├── long-task │ ├── Readme.md │ └── index.js ├── make-lazy-img │ ├── Readme.md │ ├── images │ │ └── check-lazy-imgs.PNG │ └── index.js ├── optimize-svg-usage │ ├── Readme.md │ └── index.ts ├── re-apply-dom │ ├── Readme.md │ └── index.js ├── resources-hints │ ├── Readme.md │ └── index.js ├── scripts-loading │ ├── Readme.md │ └── index.js ├── scroll-up-down │ ├── Readme.md │ └── index.js ├── time-to-first-byte-all │ ├── Readme.md │ └── index.js ├── time-to-first-byte-document │ ├── Readme.md │ └── index.js ├── tsconfig.base.json ├── tsconfig.docs.json ├── tsconfig.lib.json ├── types.ts └── utils.ts └── time-stats ├── README.md ├── images └── img.png ├── index.js ├── timer ├── timers_flag_set.json └── timers_full_zone.json └── xhr ├── xhr_flags_set.json └── xhr_full_zone.json /.github/workflows/update-readme.yml: -------------------------------------------------------------------------------- 1 | # This file was auto-generated by the Firebase CLI 2 | # https://github.com/firebase/firebase-tools 3 | 4 | name: Update readme on merge 5 | on: 6 | push: 7 | branches: [ "main" ] 8 | pull_request: 9 | branches: [ "main" ] 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout the repository 15 | uses: actions/checkout@v3 16 | 17 | - name: Setup Node.js 18 | uses: actions/setup-node@v3 19 | with: 20 | node-version: '16' 21 | cache: 'npm' 22 | - run: npm ci 23 | - name: Update snippets in main readme 24 | run: npm run docs:build 25 | - name: Commit measures 26 | uses: EndBug/add-and-commit@v7 27 | with: 28 | author_name: push-based-bot 29 | author_email: opensource@push-based.io 30 | message: 'Update snippets in main readme' 31 | add: '*' 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | node_modules 3 | dist 4 | user-data 5 | -------------------------------------------------------------------------------- /chromium/bin.ts: -------------------------------------------------------------------------------- 1 | import * as pptr from 'puppeteer'; 2 | import {createBookmarkFile, loadSnippets, toBookletName} from "./bookmarks/utils"; 3 | import {USER_DATA_DIR} from "./constants"; 4 | import {dirname} from "path"; 5 | import {SNIPPETS_ROOT} from "../snippets"; 6 | 7 | (async () => { 8 | const userDataDir = (process.argv as any).p || USER_DATA_DIR; 9 | console.info('userDataDir: ', userDataDir); 10 | const url = (process.argv as any)[2] || ''; 11 | console.info('URL: ', process.argv); 12 | createBookmarkFile({ 13 | bookmarkBar: loadSnippets(SNIPPETS_ROOT) 14 | .map(({fileName, javascript}) => ({name: toBookletName(dirname(fileName)), javascript})), 15 | userDataDir 16 | }); 17 | const browser = await pptr.launch({ 18 | headless: false, 19 | userDataDir 20 | }); 21 | 22 | const page = await browser.newPage(); 23 | if(url) { 24 | await page.goto(url) 25 | } else { 26 | console.info('No URL given to navigate'); 27 | } 28 | console.log('Chromium launched!'); 29 | 30 | 31 | })() 32 | -------------------------------------------------------------------------------- /chromium/bookmarks/constants.ts: -------------------------------------------------------------------------------- 1 | export const DEFAULT_FILENAME = 'index.js'; 2 | -------------------------------------------------------------------------------- /chromium/bookmarks/index.ts: -------------------------------------------------------------------------------- 1 | export * from './constants'; 2 | export * from './utils'; 3 | export * from './types'; 4 | -------------------------------------------------------------------------------- /chromium/bookmarks/types.ts: -------------------------------------------------------------------------------- 1 | export type BookMarkContent = { name: string, javascript: string }; 2 | 3 | export interface MetaInfo { 4 | power_bookmark_meta: string; 5 | } 6 | 7 | export interface Child { 8 | date_added: string; 9 | date_last_used: string; 10 | guid: string; 11 | id: string; 12 | meta_info: MetaInfo; 13 | name: string; 14 | type: string; 15 | url: string; 16 | } 17 | 18 | export interface BookmarkBar { 19 | children: Child[]; 20 | date_added: string; 21 | date_last_used: string; 22 | date_modified: string; 23 | guid: string; 24 | id: string; 25 | name: string; 26 | type: string; 27 | } 28 | 29 | export interface Other { 30 | children: any[]; 31 | date_added: string; 32 | date_last_used: string; 33 | date_modified: string; 34 | guid: string; 35 | id: string; 36 | name: string; 37 | type: string; 38 | } 39 | 40 | export interface Synced { 41 | children: any[]; 42 | date_added: string; 43 | date_last_used: string; 44 | date_modified: string; 45 | guid: string; 46 | id: string; 47 | name: string; 48 | type: string; 49 | } 50 | 51 | export interface Roots { 52 | bookmark_bar: BookmarkBar; 53 | other: Other; 54 | synced: Synced; 55 | } 56 | 57 | export interface RootObject { 58 | checksum: string; 59 | roots: Roots; 60 | version: number; 61 | } 62 | 63 | -------------------------------------------------------------------------------- /chromium/bookmarks/utils.ts: -------------------------------------------------------------------------------- 1 | import {BookMarkContent, Child, RootObject} from "./types"; 2 | import {lstatSync, mkdirSync, readdirSync, readFileSync, writeFileSync} from "fs"; 3 | import {dirname, join} from "path"; 4 | import {DEFAULT_FILENAME} from "./constants"; 5 | 6 | function createBookMark(bookmarkContent: BookMarkContent): Child { 7 | return ({ 8 | date_added: '13313299914044308', 9 | date_last_used: '0', 10 | guid: '9bd27f0a-554d-44c4-a64d-ba7cccf97ae5', 11 | id: '2', 12 | meta_info: { 13 | power_bookmark_meta: '' 14 | }, 15 | name: bookmarkContent.name, 16 | type: 'url', 17 | url: `javascript:(()=>{${bookmarkContent.javascript}})();` 18 | }); 19 | } 20 | 21 | export function createBookmarkFile(cfg: { 22 | bookmarkBar: BookMarkContent[], 23 | userDataDir: string 24 | }): void { 25 | const content: RootObject = { 26 | checksum: '92c6dd5c89c7da4a391f53cece274c6a', 27 | roots: { 28 | bookmark_bar: { 29 | children: cfg.bookmarkBar.map(createBookMark), 30 | date_added: '13313299898938408', 31 | date_last_used: '0', 32 | date_modified: '13313299914044308', 33 | guid: '0bc5d13f-2cba-5d74-951f-3f233fe6c908', 34 | id: '1', 35 | name: 'Lesezeichenleiste', 36 | type: 'folder' 37 | }, 38 | other: { 39 | children: [], 40 | date_added: '13313299898938415', 41 | date_last_used: '0', 42 | date_modified: '0', 43 | guid: '82b081ec-3dd3-529c-8475-ab6c344590dd', 44 | id: '3', 45 | name: 'Weitere Lesezeichen', 46 | type: 'folder' 47 | }, 48 | synced: { 49 | children: [], 50 | date_added: '13313299898938418', 51 | date_last_used: '0', 52 | date_modified: '0', 53 | guid: '4cf2e351-0e85-532b-bb37-df045d8f8d0f', 54 | id: '4', 55 | name: 'Mobile Lesezeichen', 56 | 'type': 'folder' 57 | } 58 | }, 59 | version: 1 60 | } 61 | 62 | const userDataDir = join(cfg.userDataDir, 'Default'); 63 | const userDataPath = join(userDataDir, 'Bookmarks'); 64 | 65 | mkdirSync(userDataDir, {recursive: true}) 66 | writeFileSync(userDataPath, JSON.stringify(content)); 67 | } 68 | 69 | function normalizePath(path: string): string[] { 70 | const stats = lstatSync(path); 71 | if (stats.isFile()) { 72 | return [path]; 73 | } else { 74 | return readdirSync(path).flatMap(p => normalizePath(join(path, p))) 75 | } 76 | } 77 | 78 | export function getSnippetPaths(folder: string, isScript: (fileName: string) => boolean = (f) => f.endsWith(DEFAULT_FILENAME)): string[] { 79 | return normalizePath(folder) 80 | .filter(isScript) 81 | } 82 | 83 | export function loadSnippets(folder: string, isScript: (fileName: string) => boolean = (f) => f.endsWith(DEFAULT_FILENAME)): ({ fileName: string } & Pick)[] { 84 | 85 | const bookmarkContent = getSnippetPaths(folder, isScript) 86 | .map(fileName => { 87 | return {fileName, javascript: readFileSync(fileName, 'utf8')}; 88 | }) 89 | console.log(`We found ${bookmarkContent.length} snippets`); 90 | return bookmarkContent; 91 | } 92 | 93 | export function toBookletName(folderName: string): string { 94 | let name = folderName.split('\\').pop()?.replace(/([a-z])([A-Z])/g, '$1 $2').replace(/([-_])/g, ' ') as string; 95 | console.log('name: ', name[0].toUpperCase() + name.slice(1)); 96 | return name[0].toUpperCase() + name.slice(1); 97 | } 98 | -------------------------------------------------------------------------------- /chromium/constants.ts: -------------------------------------------------------------------------------- 1 | export const USER_DATA_DIR = './user-data'; 2 | -------------------------------------------------------------------------------- /chromium/index.ts: -------------------------------------------------------------------------------- 1 | export * from './constants'; 2 | export * from './bookmarks/index'; 3 | -------------------------------------------------------------------------------- /chromium/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@push-based/perf-bookmarks-chromium", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "author": "michael Hladky", 7 | "license": "mit", 8 | "dependencies": {}, 9 | "devDependencies": { 10 | "puppeteer": "^19.3.0" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /chromium/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "rootDir": ".", 4 | "outDir": "../dist/chromium", 5 | "lib": [ 6 | "es2019", 7 | "es2020.bigint", 8 | "es2020.string", 9 | "es2020.symbol.wellknown" 10 | ], 11 | "module": "commonjs", 12 | "target": "es2019", 13 | "allowJs": true 14 | }, 15 | "include": [ 16 | "**/index.js" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /docs/autohotkey.md: -------------------------------------------------------------------------------- 1 | How to use Autohotkey example 2 | 3 | 1. Download and install [Autohotkey](https://www.autohotkey.com/) 4 | 2. Download the Autohotkey script: `quick-script-click.ahk` 5 | 6 | How to use 7 | 1. Run the `quick-script-click.ahk` which will start autohotkey and load this script 8 | * (Autohotkey doesn't detect changes to the file unless you rerun it) 9 | 2. Prepare your browser 10 | 1. Open performance tab and have Quick Source enabled 11 | 2. Ensure that the script you want to run has been selected in the Quick Sources tab 12 | 3. Position mouse curser on the Quick Source tab 13 | 4. Press WIN + n 14 | 15 | ![Autohotkey](./images/autohotkey-example.png) 16 | -------------------------------------------------------------------------------- /docs/constants.ts: -------------------------------------------------------------------------------- 1 | export const NEW_LINE = "\r\n"; 2 | export const SNIPPET_AREA_START = ''; 3 | export const SNIPPET_AREA_END = ''; 4 | -------------------------------------------------------------------------------- /docs/how-to-use-it-with-bookmarks.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | To use a code snippet as bookmark: 4 | 1. Minify code using for example [javascript-minifier](https://javascript-minifier.com/) 5 | 2. Create bookmark snippet 6 | 1. prefix it with `javascript:` 7 | 2. wrap it inside an [IIFE](https://developer.mozilla.org/de/docs/Glossary/IIFE) `(()=>{ ... })();` 8 | 9 | Example: 10 | `javascript:(()=>{ console.log('logged in bookmark') })();` 11 | 12 | 3. Create a new bookmark and paste wrapped result in URL field. 13 | 14 | 4. Add Bookmark 15 | 1. Show bookmarks 16 | `Settings => Bookmarks => Show bookmarks` or `STRG+SHIFT+B` 17 | 2. Right click on the bookmark panel 18 | 3. Add Page 19 | 4. Insert title and snippet 20 | [Bookmark Title](./images/bookmarks-edit.png) 21 | 22 | **hint** 23 | The bookmarked script will only be executed when you visit a page. If you e.g. open 24 | Chrome and no page is open, the script will not be executed. 25 | -------------------------------------------------------------------------------- /docs/how-to-use-it-with-chromium.md: -------------------------------------------------------------------------------- 1 | ## Run a snippet with [Chromium](https://www.chromium.org/chromium-projects/) 2 | 3 | To use is as bookmarks in a fresh chromium: 4 | 5 | 1. run `npm i` 6 | 2. run: `npm run bookmarks`; 7 | chromuim 8 | 9 | Optionally pass a parameter to provide a initial URL: 10 | run: `npm run bookmarks --url http://www.push-based.io`; 11 | bookmarks 12 | 13 | 3. (optionally) Show bookmarks 14 | If the bookmarks are not visible open them over the menu od hit CTRL + SHIFT + B 15 | show-bookmarks 16 | 17 | -------------------------------------------------------------------------------- /docs/how-to-use-it-with-console-tab.md: -------------------------------------------------------------------------------- 1 | # How to use snippets in the console 2 | 3 | 1. Open the DevTools (CTRL + SHIFT + I ) 4 | 2. Open the Console Tab 5 | 3. Copy snippet by clicking the copy icon on the code box e.g. 6 | 7 | ```js 8 | const imgs = document.querySelectorAll('img'); 9 | const eager = Array.from(imgs) 10 | .map(i => i.getAttribute('loading')) 11 | .filter(l => !l).length; 12 | console.log(eager+ ' of ' + imgs.length + ' img\'s eager (LCP included)'); 13 | document.title= eager+ ' of ' + imgs.length + ' img\'s eager (LCP included)'; 14 | ``` 15 | 16 | 4. Past the content in the (CTRL + C) console and hit ENTER 17 | 18 | ![console execution](https://user-images.githubusercontent.com/4904455/206262209-ad67fc96-ae3c-4b5b-afe8-e6847af7cc93.png) 19 | -------------------------------------------------------------------------------- /docs/how-to-use-it-with-sources-tab.md: -------------------------------------------------------------------------------- 1 | # How to use snippets with the Sources Tab 2 | 3 | 1. Open DevTools 4 | 2. Select Sources Tab 5 | 3. Select Snippets 6 | 4. Click New snippet 7 | 5. Give it a name 8 | 6. Insert scripts 9 | 7. Execute 10 | 8. Check console 11 | 12 | Open the DevTools at the Sources Tab. 13 | 14 | ![Open sources tab](https://user-images.githubusercontent.com/4904455/206261169-0835f6df-5678-4c3f-8a13-218df4a0b7c3.png) 15 | 16 | Add the code as new snippet to your dev tools. 17 | 18 | ![add snippet](https://user-images.githubusercontent.com/4904455/206261074-88bb8dba-32c3-4a94-a700-1ccb0f91b24f.png) 19 | 20 | -------------------------------------------------------------------------------- /docs/images/autohotkey-example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/push-based/awesome-web-performance-snippets/2273b69d62e12f44520a5d61cf8cd6c1fa6131b6/docs/images/autohotkey-example.png -------------------------------------------------------------------------------- /docs/images/bookmarks-edit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/push-based/awesome-web-performance-snippets/2273b69d62e12f44520a5d61cf8cd6c1fa6131b6/docs/images/bookmarks-edit.png -------------------------------------------------------------------------------- /docs/index.ts: -------------------------------------------------------------------------------- 1 | import {loadSnippets, toBookletName} from "../chromium"; 2 | import {readFileSync, writeFileSync} from "fs"; 3 | import {NEW_LINE, SNIPPET_AREA_END, SNIPPET_AREA_START} from "./constants"; 4 | import {dirname} from "path"; 5 | import {SNIPPETS_DIST, toConsoleSnippet} from "../snippets"; 6 | 7 | (() => { 8 | let readmeContent: string = readFileSync('./Readme.md', 'utf8'); 9 | const bookMarkContent: string = loadSnippets(SNIPPETS_DIST) 10 | .map(({fileName, javascript}) => { 11 | const title = toBookletName(dirname(fileName).split('/').pop()); 12 | const folder = fileName 13 | // split at wrong separator 14 | .split('\\') 15 | // remove file from path 16 | .slice(0, -1) 17 | // join with proper forward slash for url 18 | .join('/'); 19 | 20 | const h2 = `## [${title}](https://github.com/push-based/web-performance-tools/tree/main/${folder})` + NEW_LINE; 21 | const snippet = toConsoleSnippet(javascript) 22 | return h2 + snippet; 23 | }).join(''); 24 | 25 | if (bookMarkContent !== '') { 26 | const [start, _] = readmeContent.split(SNIPPET_AREA_START); 27 | if (_ === undefined) { 28 | throw new Error(`./Readme.md is missing the comments: "${SNIPPET_AREA_START}" and ${SNIPPET_AREA_END}`) 29 | } 30 | const [content, end] = _.split(SNIPPET_AREA_END); 31 | readmeContent = start + 32 | SNIPPET_AREA_START + NEW_LINE + 33 | bookMarkContent + 34 | SNIPPET_AREA_END + NEW_LINE + 35 | end; 36 | writeFileSync('./Readme.md', readmeContent, 'utf8'); 37 | } 38 | 39 | })(); 40 | -------------------------------------------------------------------------------- /docs/quick-script-click.ahk: -------------------------------------------------------------------------------- 1 | #NoEnv ; Recommended for performance and compatibility with future AutoHotkey releases. 2 | ; #Warn ; Enable warnings to assist with detecting common errors. 3 | SendMode Input ; Recommended for new scripts due to its superior speed and reliability. 4 | SetWorkingDir %A_ScriptDir% ; Ensures a consistent starting directory. 5 | 6 | ; Executed by pressing WIN + n. 7 | ; The mouse should be over the "Quick Source" of the performance tab before 8 | #n:: 9 | 10 | MouseGetPos, xpos, ypos 11 | 12 | ; Start recording and wait 500ms 13 | Send ^E 14 | Sleep 500 15 | 16 | ; Click mouse and press CTRL + Enter multiple times 17 | Loop 6 { 18 | Click, xpos ypos 19 | 20 | Send ^{Enter} 21 | 22 | Sleep 1000 23 | } 24 | 25 | Send ^E ; Stop recording 26 | Return 27 | -------------------------------------------------------------------------------- /docs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "rootDir": ".", 4 | "outDir": "../dist/docs", 5 | "lib": [ 6 | "es2019", 7 | "es2020" 8 | ], 9 | "module": "commonjs", 10 | "target": "es2019" 11 | }, 12 | "include": [ 13 | "../chromium/index.ts", 14 | "../snippets/index.ts" 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "perf-average", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "chromium:serve": "ts-node chromium/bin.ts", 8 | "docs:update": "ts-node docs/index.ts", 9 | "docs:build": "npm run snippets:build && npm run docs:update", 10 | "snippets:compile": "tsc -p ./snippets/tsconfig.lib.json", 11 | "snippets:update": "ts-node snippets/bin.ts -p ./snippets/tsconfig.docs.json", 12 | "snippets:build": "npm run snippets:compile && npm run snippets:update", 13 | "chromium:build": "tsc -p chromium/tsconfig.json", 14 | "time-stats": "node time-stats/index.js" 15 | }, 16 | "author": "Michael.Hladky@push-based.io", 17 | "license": "ISC", 18 | "dependencies": { 19 | "lodash": "^4.17.21" 20 | }, 21 | "devDependencies": { 22 | "puppeteer": "^19.3.0", 23 | "ts-node": "^10.9.1", 24 | "typescript": "^4.9.3" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /snippets/_SNIPPET_TEMPLATE/Readme.md: -------------------------------------------------------------------------------- 1 | # 2 | 3 | ## Description 4 | 5 | ## How to use it 6 | 7 | 8 | auto generated content here 9 | 10 | -------------------------------------------------------------------------------- /snippets/_SNIPPET_TEMPLATE/index.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/push-based/awesome-web-performance-snippets/2273b69d62e12f44520a5d61cf8cd6c1fa6131b6/snippets/_SNIPPET_TEMPLATE/index.ts -------------------------------------------------------------------------------- /snippets/bin.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "fs"; 2 | import * as path from "path"; 3 | import {SNIPPETS_ROOT, SNIPPETS_TEMPLATE_NAME} from "./constants"; 4 | import {updateSnippet} from "./utils"; 5 | 6 | (() => { 7 | const snippetsPath = SNIPPETS_ROOT; 8 | fs.readdirSync(snippetsPath).filter(p => { 9 | const stats = fs.lstatSync(path.join(snippetsPath, p)); 10 | return stats.isDirectory() && !p.includes(SNIPPETS_TEMPLATE_NAME); 11 | }) 12 | .forEach(updateSnippet); 13 | })(); 14 | -------------------------------------------------------------------------------- /snippets/check-first-and-third-party-script-timings/Readme.md: -------------------------------------------------------------------------------- 1 | # Check first and third party script timings 2 | 3 | ## Description 4 | 5 | > Note: 6 | > This snippet is borrowed from [github.com/nucliweb/webperf-snippets](https://github.com/nucliweb/webperf-snippets/blob/main/README.md#first-and-third-party-script-timings). 7 | 8 | _This relies on the above script_ 9 | 10 | _Run First And Third Party Script Info in the console first, then run this_ 11 | 12 | [Calculate Load Times - MDN](https://developer.mozilla.org/en-US/docs/Web/API/Resource_Timing_API/Using_the_Resource_Timing_API#timing_resource_loading_phases) 13 | 14 |
Info on CORS (why some values are 0) 15 | 16 |

17 | 18 | > Note: The properties which are returned as 0 by default when loading a resource from a domain other than the one of the web page itself: redirectStart, redirectEnd, domainLookupStart, domainLookupEnd, connectStart, connectEnd, secureConnectionStart, requestStart, and responseStart. 19 | 20 |

21 |
22 |
23 | 24 | [More Info on TAO header - Akamai Developer Resources](https://developer.akamai.com/blog/2018/06/13/how-add-timing-allow-origin-headers-improve-site-performance-measurement) 25 | ## How to use it 26 | 27 | 28 | 29 | 30 | | Technique | Is Usable | 31 | | ----------- | ---------- | 32 | | [Bookmark](https://github.com/push-based/web-performance-tools/blob/main/docs/how-to-use-it-with-bookmarks) | ✔ | 33 | | [Console Tab](https://github.com/push-based/web-performance-tools/blob/main/docs/how-to-use-it-with-console-tab.md) | ✔ | 34 | | [Sources Tab](https://github.com/push-based/web-performance-tools/blob/main/docs/how-to-use-it-with-sources-tab.md) | ✔ | 35 | | [Chromium](https://github.com/push-based/web-performance-tools/blob/main/docs/how-to-use-it-with-chromium.md) | ✔ | 36 | 37 | 38 | 39 | ### Bookmark Snippet 40 | 41 | 42 | 43 |
44 | 45 | Copy this code snippet into the bookmark to use it 46 | 47 | 48 | ```javascript 49 | 50 | javascript:(() => {function createUniqueLists(firstParty, thirdParty) { 51 | function getUniqueListBy(arr, key) { 52 | return [...new Map(arr.map((item) => [item[key], item])).values()]; 53 | } 54 | const firstPartyList = getUniqueListBy(firstParty, ["name"]); 55 | const thirdPartyList = getUniqueListBy(thirdParty, ["name"]); 56 | return { firstPartyList, thirdPartyList }; 57 | } 58 | const { firstPartyList, thirdPartyList } = createUniqueLists(firstParty, thirdParty); 59 | function calculateTimings(party, type) { 60 | const partyChoice = party === "first" ? firstParty : thirdParty; 61 | const timingChoices = { 62 | DNS_TIME: ["domainLookupEnd", "domainLookupStart"], 63 | TCP_HANDSHAKE: ["connectEnd", "connectStart"], 64 | RESPONSE_TIME: ["responseEnd", "responseStart"], 65 | SECURE_CONNECTION_TIME: ["connectEnd", "secureConnectionStart", 0], 66 | FETCH_UNTIL_RESPONSE: ["responseEnd", "fetchStart", 0], 67 | REQ_START_UNTIL_RES_END: ["responseEnd", "requestStart", 0], 68 | START_UNTIL_RES_END: ["responseEnd", "startTime", 0], 69 | REDIRECT_TIME: ["redirectEnd", "redirectStart"], 70 | }; 71 | function handleChoices(timingEnd, timingStart, num) { 72 | if (!num) { 73 | return timingEnd - timingStart; 74 | } 75 | if (timingStart > 0) { 76 | return timingEnd - timingStart; 77 | } 78 | return 0; 79 | } 80 | const timings = partyChoice.map((script) => { 81 | const [timingEnd, timingStart, num] = timingChoices[type]; 82 | const endValue = script[timingEnd]; 83 | const startValue = script[timingStart]; 84 | return { 85 | name: script.name, 86 | [type]: handleChoices(endValue, startValue, num), 87 | }; 88 | }); 89 | return timings; 90 | } 91 | // Available Options 92 | const timingOptions = [ 93 | "DNS_TIME", 94 | "TCP_HANDSHAKE", 95 | "RESPONSE_TIME", 96 | "SECURE_CONNECTION_TIME", 97 | "FETCH_UNTIL_RESPONSE", 98 | "REQ_START_UNTIL_RES_END", 99 | "START_UNTIL_RES_END", 100 | "REDIRECT_TIME", 101 | ]; 102 | // run em all! 103 | // https://developer.mozilla.org/en-US/docs/Web/API/Resource_Timing_API/Using_the_Resource_Timing_API#timing_resource_loading_phases 104 | timingOptions.forEach((timing) => { 105 | console.groupCollapsed(`FIRST PARTY: ${timing}`); 106 | console.table(calculateTimings("first", timing)); 107 | console.groupEnd(); 108 | console.groupCollapsed(`THIRD PARTY: ${timing}`); 109 | console.table(calculateTimings("third", timing)); 110 | console.groupEnd(); 111 | }); 112 | // choose your battle - arg1 is string either "first" or "third", arg2 is string timing option listed above. 113 | console.table(calculateTimings("first", "REQ_START_UNTIL_RES_END")); 114 | })() 115 | ``` 116 | 117 | 118 | 119 | 120 |
121 | 122 | 123 | 124 | ## Console Tab Snippet 125 | 126 |
127 | 128 | Copy this code snippet into the DevTools console Tab to use it 129 | 130 | 131 | ```javascript 132 | 133 | function createUniqueLists(firstParty, thirdParty) { 134 | function getUniqueListBy(arr, key) { 135 | return [...new Map(arr.map((item) => [item[key], item])).values()]; 136 | } 137 | const firstPartyList = getUniqueListBy(firstParty, ["name"]); 138 | const thirdPartyList = getUniqueListBy(thirdParty, ["name"]); 139 | return { firstPartyList, thirdPartyList }; 140 | } 141 | const { firstPartyList, thirdPartyList } = createUniqueLists(firstParty, thirdParty); 142 | function calculateTimings(party, type) { 143 | const partyChoice = party === "first" ? firstParty : thirdParty; 144 | const timingChoices = { 145 | DNS_TIME: ["domainLookupEnd", "domainLookupStart"], 146 | TCP_HANDSHAKE: ["connectEnd", "connectStart"], 147 | RESPONSE_TIME: ["responseEnd", "responseStart"], 148 | SECURE_CONNECTION_TIME: ["connectEnd", "secureConnectionStart", 0], 149 | FETCH_UNTIL_RESPONSE: ["responseEnd", "fetchStart", 0], 150 | REQ_START_UNTIL_RES_END: ["responseEnd", "requestStart", 0], 151 | START_UNTIL_RES_END: ["responseEnd", "startTime", 0], 152 | REDIRECT_TIME: ["redirectEnd", "redirectStart"], 153 | }; 154 | function handleChoices(timingEnd, timingStart, num) { 155 | if (!num) { 156 | return timingEnd - timingStart; 157 | } 158 | if (timingStart > 0) { 159 | return timingEnd - timingStart; 160 | } 161 | return 0; 162 | } 163 | const timings = partyChoice.map((script) => { 164 | const [timingEnd, timingStart, num] = timingChoices[type]; 165 | const endValue = script[timingEnd]; 166 | const startValue = script[timingStart]; 167 | return { 168 | name: script.name, 169 | [type]: handleChoices(endValue, startValue, num), 170 | }; 171 | }); 172 | return timings; 173 | } 174 | // Available Options 175 | const timingOptions = [ 176 | "DNS_TIME", 177 | "TCP_HANDSHAKE", 178 | "RESPONSE_TIME", 179 | "SECURE_CONNECTION_TIME", 180 | "FETCH_UNTIL_RESPONSE", 181 | "REQ_START_UNTIL_RES_END", 182 | "START_UNTIL_RES_END", 183 | "REDIRECT_TIME", 184 | ]; 185 | // run em all! 186 | // https://developer.mozilla.org/en-US/docs/Web/API/Resource_Timing_API/Using_the_Resource_Timing_API#timing_resource_loading_phases 187 | timingOptions.forEach((timing) => { 188 | console.groupCollapsed(`FIRST PARTY: ${timing}`); 189 | console.table(calculateTimings("first", timing)); 190 | console.groupEnd(); 191 | console.groupCollapsed(`THIRD PARTY: ${timing}`); 192 | console.table(calculateTimings("third", timing)); 193 | console.groupEnd(); 194 | }); 195 | // choose your battle - arg1 is string either "first" or "third", arg2 is string timing option listed above. 196 | console.table(calculateTimings("first", "REQ_START_UNTIL_RES_END")); 197 | 198 | ``` 199 | 200 | 201 | 202 | 203 |
204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | # Credits 314 | 315 | Author: _Joan León_ 316 | Source: _[github.com/nucliweb/webperf-snippets](https://github.com/nucliweb/webperf-snippets/blob/main/README.md#first-and-third-party-script-info)_ 317 | -------------------------------------------------------------------------------- /snippets/check-first-and-third-party-script-timings/index.js: -------------------------------------------------------------------------------- 1 | function createUniqueLists(firstParty, thirdParty) { 2 | function getUniqueListBy(arr, key) { 3 | return [...new Map(arr.map((item) => [item[key], item])).values()]; 4 | } 5 | 6 | const firstPartyList = getUniqueListBy(firstParty, ["name"]); 7 | const thirdPartyList = getUniqueListBy(thirdParty, ["name"]); 8 | 9 | return { firstPartyList, thirdPartyList }; 10 | 11 | } 12 | 13 | const { firstPartyList, thirdPartyList } = createUniqueLists( 14 | firstParty, 15 | thirdParty 16 | ); 17 | 18 | 19 | 20 | function calculateTimings(party, type) { 21 | const partyChoice = party === "first" ? firstParty : thirdParty; 22 | 23 | const timingChoices = { 24 | DNS_TIME: ["domainLookupEnd", "domainLookupStart"], 25 | TCP_HANDSHAKE: ["connectEnd", "connectStart"], 26 | RESPONSE_TIME: ["responseEnd", "responseStart"], 27 | SECURE_CONNECTION_TIME: ["connectEnd", "secureConnectionStart", 0], 28 | FETCH_UNTIL_RESPONSE: ["responseEnd", "fetchStart", 0], 29 | REQ_START_UNTIL_RES_END: ["responseEnd", "requestStart", 0], 30 | START_UNTIL_RES_END: ["responseEnd", "startTime", 0], 31 | REDIRECT_TIME: ["redirectEnd", "redirectStart"], 32 | }; 33 | 34 | function handleChoices(timingEnd, timingStart, num) { 35 | if (!num) { 36 | return timingEnd - timingStart; 37 | } 38 | 39 | if (timingStart > 0) { 40 | return timingEnd - timingStart; 41 | } 42 | 43 | return 0; 44 | } 45 | 46 | const timings = partyChoice.map((script) => { 47 | const [timingEnd, timingStart, num] = timingChoices[type]; 48 | const endValue = script[timingEnd]; 49 | const startValue = script[timingStart]; 50 | return { 51 | name: script.name, 52 | [type]: handleChoices(endValue, startValue, num), 53 | }; 54 | }); 55 | 56 | return timings; 57 | } 58 | 59 | // Available Options 60 | const timingOptions = [ 61 | "DNS_TIME", 62 | "TCP_HANDSHAKE", 63 | "RESPONSE_TIME", 64 | "SECURE_CONNECTION_TIME", 65 | "FETCH_UNTIL_RESPONSE", 66 | "REQ_START_UNTIL_RES_END", 67 | "START_UNTIL_RES_END", 68 | "REDIRECT_TIME", 69 | ]; 70 | 71 | // run em all! 72 | // https://developer.mozilla.org/en-US/docs/Web/API/Resource_Timing_API/Using_the_Resource_Timing_API#timing_resource_loading_phases 73 | 74 | timingOptions.forEach((timing) => { 75 | console.groupCollapsed(`FIRST PARTY: ${timing}`); 76 | console.table(calculateTimings("first", timing)); 77 | console.groupEnd(); 78 | console.groupCollapsed(`THIRD PARTY: ${timing}`); 79 | console.table(calculateTimings("third", timing)); 80 | console.groupEnd(); 81 | }); 82 | 83 | // choose your battle - arg1 is string either "first" or "third", arg2 is string timing option listed above. 84 | 85 | console.table(calculateTimings("first", "REQ_START_UNTIL_RES_END")); 86 | -------------------------------------------------------------------------------- /snippets/check-first-and-third-party-script/Readme.md: -------------------------------------------------------------------------------- 1 | # Check first and third party script 2 | 3 | ## Description 4 | 5 | List all scripts using PerformanceResourceTiming API and separating them by first and third party 6 | 7 | [More Info](https://developer.mozilla.org/en-US/docs/Web/API/PerformanceResourceTiming) 8 | 9 | [Info On CORS](https://developer.mozilla.org/en-US/docs/Web/API/Resource_Timing_API/Using_the_Resource_Timing_API#coping_with_cors) 10 | 11 | ## How to use it 12 | 13 | 14 | 15 | 16 | | Technique | Is Usable | 17 | | ----------- | ---------- | 18 | | [Bookmark](https://github.com/push-based/web-performance-tools/blob/main/docs/how-to-use-it-with-bookmarks) | ✔ | 19 | | [Console Tab](https://github.com/push-based/web-performance-tools/blob/main/docs/how-to-use-it-with-console-tab.md) | ✔ | 20 | | [Sources Tab](https://github.com/push-based/web-performance-tools/blob/main/docs/how-to-use-it-with-sources-tab.md) | ✔ | 21 | | [Chromium](https://github.com/push-based/web-performance-tools/blob/main/docs/how-to-use-it-with-chromium.md) | ✔ | 22 | 23 | 24 | 25 | ### Bookmark Snippet 26 | 27 | 28 | 29 |
30 | 31 | Copy this code snippet into the bookmark to use it 32 | 33 | 34 | ```javascript 35 | 36 | javascript:(() => {// ex: katespade.com - list firsty party subdomains in HOSTS array 37 | const HOSTS = ["assets.katespade.com"]; 38 | function getScriptInfo() { 39 | const resourceListEntries = performance.getEntriesByType("resource"); 40 | // set for first party scripts 41 | const first = []; 42 | // set for third party scripts 43 | const third = []; 44 | resourceListEntries.forEach((resource) => { 45 | // check for initiator type 46 | const value = "initiatorType" in resource; 47 | if (value) { 48 | if (resource.initiatorType === "script") { 49 | const { host } = new URL(resource.name); 50 | // check if resource url host matches location.host = first party script 51 | if (host === location.host || HOSTS.includes(host)) { 52 | first.push({ ...resource.toJSON(), type: "First Party" }); 53 | } 54 | else { 55 | // add to third party script 56 | third.push({ ...resource.toJSON(), type: "Third Party" }); 57 | } 58 | } 59 | } 60 | }); 61 | const scripts = { 62 | firstParty: [{ name: "no data" }], 63 | thirdParty: [{ name: "no data" }], 64 | }; 65 | if (first.length) { 66 | scripts.firstParty = first; 67 | } 68 | if (third.length) { 69 | scripts.thirdParty = third; 70 | } 71 | return scripts; 72 | } 73 | const { firstParty, thirdParty } = getScriptInfo(); 74 | console.groupCollapsed("FIRST PARTY SCRIPTS"); 75 | console.table(firstParty); 76 | console.groupEnd(); 77 | console.groupCollapsed("THIRD PARTY SCRIPTS"); 78 | console.table(thirdParty); 79 | console.groupEnd(); 80 | export {}; 81 | /* 82 | Choose which properties to display 83 | https://developer.mozilla.org/en-US/docs/Web/API/console/table 84 | 85 | console.groupCollapsed("FIRST PARTY SCRIPTS"); 86 | console.table(firstParty, ["name", "nextHopProtocol"]); 87 | console.groupEnd(); 88 | console.groupCollapsed("THIRD PARTY SCRIPTS", ["name", "nextHopProtocol"]); 89 | console.table(thirdParty); 90 | console.groupEnd(); 91 | */ 92 | })() 93 | ``` 94 | 95 | 96 | 97 | 98 |
99 | 100 | 101 | 102 | ## Console Tab Snippet 103 | 104 |
105 | 106 | Copy this code snippet into the DevTools console Tab to use it 107 | 108 | 109 | ```javascript 110 | 111 | // ex: katespade.com - list firsty party subdomains in HOSTS array 112 | const HOSTS = ["assets.katespade.com"]; 113 | function getScriptInfo() { 114 | const resourceListEntries = performance.getEntriesByType("resource"); 115 | // set for first party scripts 116 | const first = []; 117 | // set for third party scripts 118 | const third = []; 119 | resourceListEntries.forEach((resource) => { 120 | // check for initiator type 121 | const value = "initiatorType" in resource; 122 | if (value) { 123 | if (resource.initiatorType === "script") { 124 | const { host } = new URL(resource.name); 125 | // check if resource url host matches location.host = first party script 126 | if (host === location.host || HOSTS.includes(host)) { 127 | first.push({ ...resource.toJSON(), type: "First Party" }); 128 | } 129 | else { 130 | // add to third party script 131 | third.push({ ...resource.toJSON(), type: "Third Party" }); 132 | } 133 | } 134 | } 135 | }); 136 | const scripts = { 137 | firstParty: [{ name: "no data" }], 138 | thirdParty: [{ name: "no data" }], 139 | }; 140 | if (first.length) { 141 | scripts.firstParty = first; 142 | } 143 | if (third.length) { 144 | scripts.thirdParty = third; 145 | } 146 | return scripts; 147 | } 148 | const { firstParty, thirdParty } = getScriptInfo(); 149 | console.groupCollapsed("FIRST PARTY SCRIPTS"); 150 | console.table(firstParty); 151 | console.groupEnd(); 152 | console.groupCollapsed("THIRD PARTY SCRIPTS"); 153 | console.table(thirdParty); 154 | console.groupEnd(); 155 | 156 | /* 157 | Choose which properties to display 158 | https://developer.mozilla.org/en-US/docs/Web/API/console/table 159 | 160 | console.groupCollapsed("FIRST PARTY SCRIPTS"); 161 | console.table(firstParty, ["name", "nextHopProtocol"]); 162 | console.groupEnd(); 163 | console.groupCollapsed("THIRD PARTY SCRIPTS", ["name", "nextHopProtocol"]); 164 | console.table(thirdParty); 165 | console.groupEnd(); 166 | */ 167 | 168 | ``` 169 | 170 | 171 | 172 | 173 |
174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | -------------------------------------------------------------------------------- /snippets/check-first-and-third-party-script/index.ts: -------------------------------------------------------------------------------- 1 | import {EntryType} from "perf_hooks"; 2 | 3 | // ex: katespade.com - list firsty party subdomains in HOSTS array 4 | const HOSTS = ["assets.katespade.com"]; 5 | 6 | function getScriptInfo(): { 7 | firstParty: { name: string }[], 8 | thirdParty: { name: string }[] 9 | } { 10 | const resourceListEntries = performance.getEntriesByType("resource" as EntryType); 11 | // set for first party scripts 12 | const first = []; 13 | // set for third party scripts 14 | const third = []; 15 | 16 | resourceListEntries.forEach((resource) => { 17 | // check for initiator type 18 | const value = "initiatorType" in resource; 19 | if (value) { 20 | if (resource.initiatorType === "script") { 21 | const { host } = new URL(resource.name); 22 | // check if resource url host matches location.host = first party script 23 | if (host === location.host || HOSTS.includes(host)) { 24 | first.push({ ...resource.toJSON(), type: "First Party" }); 25 | } else { 26 | // add to third party script 27 | third.push({ ...resource.toJSON(), type: "Third Party" }); 28 | } 29 | } 30 | } 31 | }); 32 | 33 | const scripts = { 34 | firstParty: [{ name: "no data" }], 35 | thirdParty: [{ name: "no data" }], 36 | }; 37 | 38 | if (first.length) { 39 | scripts.firstParty = first; 40 | } 41 | 42 | if (third.length) { 43 | scripts.thirdParty = third; 44 | } 45 | 46 | return scripts; 47 | } 48 | 49 | const { firstParty, thirdParty } = getScriptInfo(); 50 | 51 | console.groupCollapsed("FIRST PARTY SCRIPTS"); 52 | console.table(firstParty); 53 | console.groupEnd(); 54 | console.groupCollapsed("THIRD PARTY SCRIPTS"); 55 | console.table(thirdParty); 56 | console.groupEnd(); 57 | 58 | /* 59 | Choose which properties to display 60 | https://developer.mozilla.org/en-US/docs/Web/API/console/table 61 | 62 | console.groupCollapsed("FIRST PARTY SCRIPTS"); 63 | console.table(firstParty, ["name", "nextHopProtocol"]); 64 | console.groupEnd(); 65 | console.groupCollapsed("THIRD PARTY SCRIPTS", ["name", "nextHopProtocol"]); 66 | console.table(thirdParty); 67 | console.groupEnd(); 68 | */ 69 | -------------------------------------------------------------------------------- /snippets/check-header/Readme.md: -------------------------------------------------------------------------------- 1 | # Check Header 2 | 3 | ## Description 4 | 5 | You can fins a detailed description in Harrys repository called [ct](https://github.com/csswizardry/ct#readme). 6 | 7 | ## How to use it 8 | 9 | 10 | 11 | 12 | 13 | 14 | ### Bookmark Snippet 15 | 16 | 17 | 18 |
19 | 20 | Copy this code snippet into the bookmark to use it 21 | 22 | 23 | ```javascript 24 | 25 | javascript:(() => {(function () { 26 | var ct = document.createElement('style'); 27 | ct.innerText = ` 28 | /*!========================================================================== 29 | #CT.CSS 30 | ========================================================================== */ 31 | 32 | /*! 33 | * ct.css – Let’s take a look inside your … 34 | * 35 | * © Harry Roberts 2021 – twitter.com/csswizardry 36 | */ 37 | 38 | 39 | 40 | 41 | 42 | /** 43 | * It’s slightly easier to remember topics than it is colours. Set up some 44 | * custom properties for use later on. 45 | */ 46 | 47 | head { 48 | --ct-is-problematic: solid; 49 | --ct-is-affected: dashed; 50 | --ct-notify: #0bce6b; 51 | --ct-warn: #ffa400; 52 | --ct-error: #ff4e42; 53 | }/** 54 | * Show the and set up the items we might be interested in. 55 | */ 56 | 57 | head, 58 | head script, 59 | head script:not([src])[async], 60 | head script:not([src])[defer], 61 | head style, head [rel="stylesheet"], 62 | head script ~ meta[http-equiv="content-security-policy"], 63 | head > meta[charset]:not(:nth-child(-n+5)) { 64 | display: block; 65 | } 66 | 67 | head script, 68 | head style, head [rel="stylesheet"], 69 | head title, 70 | head script ~ meta[http-equiv="content-security-policy"], 71 | head > meta[charset]:not(:nth-child(-n+5)) { 72 | margin: 5px; 73 | padding: 5px; 74 | border-width: 5px; 75 | background-color: white; 76 | color: #333; 77 | } 78 | 79 | head ::before, 80 | head script, head style { 81 | font: 16px/1.5 monospace, monospace; 82 | display: block; 83 | } 84 | 85 | head ::before { 86 | font-weight: bold; 87 | }/** 88 | * External Script and Style 89 | */ 90 | 91 | head script[src], 92 | head link[rel="stylesheet"] { 93 | border-style: var(--ct-is-problematic); 94 | border-color: var(--ct-warn); 95 | } 96 | 97 | head script[src]::before { 98 | content: "[Blocking Script – " attr(src) "]" 99 | } 100 | 101 | head link[rel="stylesheet"]::before { 102 | content: "[Blocking Stylesheet – " attr(href) "]" 103 | }/** 104 | * Inline Script and Style. 105 | */ 106 | 107 | head style:not(:empty), 108 | head script:not(:empty) { 109 | max-height: 5em; 110 | overflow: auto; 111 | background-color: #ffd; 112 | white-space: pre; 113 | border-color: var(--ct-notify); 114 | border-style: var(--ct-is-problematic); 115 | } 116 | 117 | head script:not(:empty)::before { 118 | content: "[Inline Script] "; 119 | } 120 | 121 | head style:not(:empty)::before { 122 | content: "[Inline Style] "; 123 | }/** 124 | * Blocked Title. 125 | * 126 | * These selectors are generally more complex because the Key Selector (\`title\`) 127 | * depends on the specific conditions of preceding JS--we can’t cast a wide net 128 | * and narrow it down later as we can when targeting elements directly. 129 | */ 130 | 131 | head script[src]:not([async]):not([defer]):not([type=module]) ~ title, 132 | head script:not(:empty) ~ title { 133 | display: block; 134 | border-style: var(--ct-is-affected); 135 | border-color: var(--ct-error); 136 | } 137 | 138 | head script[src]:not([async]):not([defer]):not([type=module]) ~ title::before, 139 | head script:not(:empty) ~ title::before { 140 | content: "[ blocked by JS] "; 141 | }/** 142 | * Blocked Scripts. 143 | * 144 | * These selectors are generally more complex because the Key Selector 145 | * (\`script\`) depends on the specific conditions of preceding CSS--we can’t cast 146 | * a wide net and narrow it down later as we can when targeting elements 147 | * directly. 148 | */ 149 | 150 | head [rel="stylesheet"]:not([media="print"]):not(.ct) ~ script, 151 | head style:not(:empty) ~ script { 152 | border-style: var(--ct-is-affected); 153 | border-color: var(--ct-warn); 154 | } 155 | 156 | head [rel="stylesheet"]:not([media="print"]):not(.ct) ~ script::before, 157 | head style:not(:empty) ~ script::before { 158 | content: "[JS blocked by CSS – " attr(src) "]"; 159 | }/** 160 | * Using both \`async\` and \`defer\` is redundant (an anti-pattern, even). Let’s 161 | * flag that. 162 | */ 163 | 164 | head script[src][src][async][defer] { 165 | display: block; 166 | border-style: var(--ct-is-problematic); 167 | border-color: var(--ct-warn); 168 | } 169 | 170 | head script[src][src][async][defer]::before { 171 | content: "[async and defer is redundant: prefer defer – " attr(src) "]"; 172 | }/** 173 | * Async and defer simply do not work on inline scripts. It won’t do any harm, 174 | * but it’s useful to know about. 175 | */ 176 | head script:not([src])[async], 177 | head script:not([src])[defer] { 178 | border-style: var(--ct-is-problematic); 179 | border-color: var(--ct-warn); 180 | } 181 | 182 | head script:not([src])[async]::before { 183 | content: "The async attribute is redundant on inline scripts" 184 | } 185 | 186 | head script:not([src])[defer]::before { 187 | content: "The defer attribute is redundant on inline scripts" 188 | }/** 189 | * Third Party blocking resources. 190 | * 191 | * Expect false-positives here… it’s a crude proxy at best. 192 | * 193 | * Selector-chaining (e.g. \`[src][src]\`) is used to bump up specificity. 194 | */ 195 | 196 | head script[src][src][src^="//"], 197 | head script[src][src][src^="http"], 198 | head [rel="stylesheet"][href^="//"], 199 | head [rel="stylesheet"][href^="http"] { 200 | border-style: var(--ct-is-problematic); 201 | border-color: var(--ct-error); 202 | } 203 | 204 | head script[src][src][src^="//"]::before, 205 | head script[src][src][src^="http"]::before { 206 | content: "[Third Party Blocking Script – " attr(src) "]"; 207 | } 208 | 209 | head [rel="stylesheet"][href^="//"]::before, 210 | head [rel="stylesheet"][href^="http"]::before { 211 | content: "[Third Party Blocking Stylesheet – " attr(href) "]"; 212 | }/** 213 | * Mid-HEAD CSP disables the Preload Scanner 214 | */ 215 | 216 | head script ~ meta[http-equiv="content-security-policy"] { 217 | border-style: var(--ct-is-problematic); 218 | border-color: var(--ct-error); 219 | } 220 | 221 | head script ~ meta[http-equiv="content-security-policy"]::before { 222 | content: "[Meta CSP defined after JS]" 223 | }/** 224 | * Charset should appear as early as possible 225 | */ 226 | head > meta[charset]:not(:nth-child(-n+5)) { 227 | border-style: var(--ct-is-problematic); 228 | border-color: var(--ct-warn); 229 | } 230 | 231 | head > meta[charset]:not(:nth-child(-n+5))::before { 232 | content: "[Charset should appear as early as possible]"; 233 | }/** 234 | * Hide all irrelevant or non-matching scripts and styles (including ct.css). 235 | * 236 | * We’re done! 237 | */ 238 | 239 | link[rel="stylesheet"][media="print"], 240 | link[rel="stylesheet"].ct, style.ct, 241 | script[async], script[defer], script[type=module] { 242 | display: none; 243 | } 244 | `; 245 | ct.classList.add('ct'); 246 | document.head.appendChild(ct); 247 | }()); 248 | })() 249 | ``` 250 | 251 | 252 | 253 | 254 | </details> 255 | 256 | 257 | 258 | ## Console Tab Snippet 259 | 260 | <details> 261 | 262 | <summary>Copy this code snippet into the DevTools console Tab to use it</summary> 263 | 264 | 265 | ```javascript 266 | 267 | (function () { 268 | var ct = document.createElement('style'); 269 | ct.innerText = ` 270 | /*!========================================================================== 271 | #CT.CSS 272 | ========================================================================== */ 273 | 274 | /*! 275 | * ct.css – Let’s take a look inside your <head>… 276 | * 277 | * © Harry Roberts 2021 – twitter.com/csswizardry 278 | */ 279 | 280 | 281 | 282 | 283 | 284 | /** 285 | * It’s slightly easier to remember topics than it is colours. Set up some 286 | * custom properties for use later on. 287 | */ 288 | 289 | head { 290 | --ct-is-problematic: solid; 291 | --ct-is-affected: dashed; 292 | --ct-notify: #0bce6b; 293 | --ct-warn: #ffa400; 294 | --ct-error: #ff4e42; 295 | }/** 296 | * Show the <head> and set up the items we might be interested in. 297 | */ 298 | 299 | head, 300 | head script, 301 | head script:not([src])[async], 302 | head script:not([src])[defer], 303 | head style, head [rel="stylesheet"], 304 | head script ~ meta[http-equiv="content-security-policy"], 305 | head > meta[charset]:not(:nth-child(-n+5)) { 306 | display: block; 307 | } 308 | 309 | head script, 310 | head style, head [rel="stylesheet"], 311 | head title, 312 | head script ~ meta[http-equiv="content-security-policy"], 313 | head > meta[charset]:not(:nth-child(-n+5)) { 314 | margin: 5px; 315 | padding: 5px; 316 | border-width: 5px; 317 | background-color: white; 318 | color: #333; 319 | } 320 | 321 | head ::before, 322 | head script, head style { 323 | font: 16px/1.5 monospace, monospace; 324 | display: block; 325 | } 326 | 327 | head ::before { 328 | font-weight: bold; 329 | }/** 330 | * External Script and Style 331 | */ 332 | 333 | head script[src], 334 | head link[rel="stylesheet"] { 335 | border-style: var(--ct-is-problematic); 336 | border-color: var(--ct-warn); 337 | } 338 | 339 | head script[src]::before { 340 | content: "[Blocking Script – " attr(src) "]" 341 | } 342 | 343 | head link[rel="stylesheet"]::before { 344 | content: "[Blocking Stylesheet – " attr(href) "]" 345 | }/** 346 | * Inline Script and Style. 347 | */ 348 | 349 | head style:not(:empty), 350 | head script:not(:empty) { 351 | max-height: 5em; 352 | overflow: auto; 353 | background-color: #ffd; 354 | white-space: pre; 355 | border-color: var(--ct-notify); 356 | border-style: var(--ct-is-problematic); 357 | } 358 | 359 | head script:not(:empty)::before { 360 | content: "[Inline Script] "; 361 | } 362 | 363 | head style:not(:empty)::before { 364 | content: "[Inline Style] "; 365 | }/** 366 | * Blocked Title. 367 | * 368 | * These selectors are generally more complex because the Key Selector (\`title\`) 369 | * depends on the specific conditions of preceding JS--we can’t cast a wide net 370 | * and narrow it down later as we can when targeting elements directly. 371 | */ 372 | 373 | head script[src]:not([async]):not([defer]):not([type=module]) ~ title, 374 | head script:not(:empty) ~ title { 375 | display: block; 376 | border-style: var(--ct-is-affected); 377 | border-color: var(--ct-error); 378 | } 379 | 380 | head script[src]:not([async]):not([defer]):not([type=module]) ~ title::before, 381 | head script:not(:empty) ~ title::before { 382 | content: "[<title> blocked by JS] "; 383 | }/** 384 | * Blocked Scripts. 385 | * 386 | * These selectors are generally more complex because the Key Selector 387 | * (\`script\`) depends on the specific conditions of preceding CSS--we can’t cast 388 | * a wide net and narrow it down later as we can when targeting elements 389 | * directly. 390 | */ 391 | 392 | head [rel="stylesheet"]:not([media="print"]):not(.ct) ~ script, 393 | head style:not(:empty) ~ script { 394 | border-style: var(--ct-is-affected); 395 | border-color: var(--ct-warn); 396 | } 397 | 398 | head [rel="stylesheet"]:not([media="print"]):not(.ct) ~ script::before, 399 | head style:not(:empty) ~ script::before { 400 | content: "[JS blocked by CSS – " attr(src) "]"; 401 | }/** 402 | * Using both \`async\` and \`defer\` is redundant (an anti-pattern, even). Let’s 403 | * flag that. 404 | */ 405 | 406 | head script[src][src][async][defer] { 407 | display: block; 408 | border-style: var(--ct-is-problematic); 409 | border-color: var(--ct-warn); 410 | } 411 | 412 | head script[src][src][async][defer]::before { 413 | content: "[async and defer is redundant: prefer defer – " attr(src) "]"; 414 | }/** 415 | * Async and defer simply do not work on inline scripts. It won’t do any harm, 416 | * but it’s useful to know about. 417 | */ 418 | head script:not([src])[async], 419 | head script:not([src])[defer] { 420 | border-style: var(--ct-is-problematic); 421 | border-color: var(--ct-warn); 422 | } 423 | 424 | head script:not([src])[async]::before { 425 | content: "The async attribute is redundant on inline scripts" 426 | } 427 | 428 | head script:not([src])[defer]::before { 429 | content: "The defer attribute is redundant on inline scripts" 430 | }/** 431 | * Third Party blocking resources. 432 | * 433 | * Expect false-positives here… it’s a crude proxy at best. 434 | * 435 | * Selector-chaining (e.g. \`[src][src]\`) is used to bump up specificity. 436 | */ 437 | 438 | head script[src][src][src^="//"], 439 | head script[src][src][src^="http"], 440 | head [rel="stylesheet"][href^="//"], 441 | head [rel="stylesheet"][href^="http"] { 442 | border-style: var(--ct-is-problematic); 443 | border-color: var(--ct-error); 444 | } 445 | 446 | head script[src][src][src^="//"]::before, 447 | head script[src][src][src^="http"]::before { 448 | content: "[Third Party Blocking Script – " attr(src) "]"; 449 | } 450 | 451 | head [rel="stylesheet"][href^="//"]::before, 452 | head [rel="stylesheet"][href^="http"]::before { 453 | content: "[Third Party Blocking Stylesheet – " attr(href) "]"; 454 | }/** 455 | * Mid-HEAD CSP disables the Preload Scanner 456 | */ 457 | 458 | head script ~ meta[http-equiv="content-security-policy"] { 459 | border-style: var(--ct-is-problematic); 460 | border-color: var(--ct-error); 461 | } 462 | 463 | head script ~ meta[http-equiv="content-security-policy"]::before { 464 | content: "[Meta CSP defined after JS]" 465 | }/** 466 | * Charset should appear as early as possible 467 | */ 468 | head > meta[charset]:not(:nth-child(-n+5)) { 469 | border-style: var(--ct-is-problematic); 470 | border-color: var(--ct-warn); 471 | } 472 | 473 | head > meta[charset]:not(:nth-child(-n+5))::before { 474 | content: "[Charset should appear as early as possible]"; 475 | }/** 476 | * Hide all irrelevant or non-matching scripts and styles (including ct.css). 477 | * 478 | * We’re done! 479 | */ 480 | 481 | link[rel="stylesheet"][media="print"], 482 | link[rel="stylesheet"].ct, style.ct, 483 | script[async], script[defer], script[type=module] { 484 | display: none; 485 | } 486 | `; 487 | ct.classList.add('ct'); 488 | document.head.appendChild(ct); 489 | }()); 490 | 491 | ``` 492 | 493 | 494 | 495 | 496 | </details> 497 | 498 | 499 | 500 | 501 | <!-- END-HOW_TO --> 502 | 503 | 504 | 505 | 506 | 507 | 508 | 509 | 510 | 511 | 512 | 513 | 514 | 515 | 516 | 517 | 518 | 519 | 520 | 521 | 522 | 523 | 524 | 525 | 526 | 527 | 528 | 529 | 530 | 531 | 532 | 533 | 534 | 535 | 536 | 537 | 538 | 539 | 540 | 541 | 542 | 543 | 544 | 545 | 546 | 547 | 548 | 549 | 550 | 551 | 552 | 553 | 554 | 555 | 556 | 557 | 558 | 559 | 560 | 561 | 562 | 563 | 564 | 565 | 566 | 567 | 568 | 569 | 570 | 571 | 572 | 573 | 574 | 575 | 576 | 577 | 578 | 579 | 580 | 581 | 582 | 583 | 584 | 585 | 586 | 587 | 588 | 589 | 590 | 591 | 592 | 593 | 594 | 595 | 596 | 597 | 598 | 599 | 600 | 601 | 602 | 603 | 604 | 605 | 606 | # Credits 607 | 608 | Author: _Harry Roberts_ 609 | Source: _[github.com/csswizardry/ct](https://github.com/csswizardry/ct)_ 610 | -------------------------------------------------------------------------------- /snippets/check-header/index.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | var ct = document.createElement('style'); 3 | ct.innerText = ` 4 | /*!========================================================================== 5 | #CT.CSS 6 | ========================================================================== */ 7 | 8 | /*! 9 | * ct.css – Let’s take a look inside your <head>… 10 | * 11 | * © Harry Roberts 2021 – twitter.com/csswizardry 12 | */ 13 | 14 | 15 | 16 | 17 | 18 | /** 19 | * It’s slightly easier to remember topics than it is colours. Set up some 20 | * custom properties for use later on. 21 | */ 22 | 23 | head { 24 | --ct-is-problematic: solid; 25 | --ct-is-affected: dashed; 26 | --ct-notify: #0bce6b; 27 | --ct-warn: #ffa400; 28 | --ct-error: #ff4e42; 29 | }/** 30 | * Show the <head> and set up the items we might be interested in. 31 | */ 32 | 33 | head, 34 | head script, 35 | head script:not([src])[async], 36 | head script:not([src])[defer], 37 | head style, head [rel="stylesheet"], 38 | head script ~ meta[http-equiv="content-security-policy"], 39 | head > meta[charset]:not(:nth-child(-n+5)) { 40 | display: block; 41 | } 42 | 43 | head script, 44 | head style, head [rel="stylesheet"], 45 | head title, 46 | head script ~ meta[http-equiv="content-security-policy"], 47 | head > meta[charset]:not(:nth-child(-n+5)) { 48 | margin: 5px; 49 | padding: 5px; 50 | border-width: 5px; 51 | background-color: white; 52 | color: #333; 53 | } 54 | 55 | head ::before, 56 | head script, head style { 57 | font: 16px/1.5 monospace, monospace; 58 | display: block; 59 | } 60 | 61 | head ::before { 62 | font-weight: bold; 63 | }/** 64 | * External Script and Style 65 | */ 66 | 67 | head script[src], 68 | head link[rel="stylesheet"] { 69 | border-style: var(--ct-is-problematic); 70 | border-color: var(--ct-warn); 71 | } 72 | 73 | head script[src]::before { 74 | content: "[Blocking Script – " attr(src) "]" 75 | } 76 | 77 | head link[rel="stylesheet"]::before { 78 | content: "[Blocking Stylesheet – " attr(href) "]" 79 | }/** 80 | * Inline Script and Style. 81 | */ 82 | 83 | head style:not(:empty), 84 | head script:not(:empty) { 85 | max-height: 5em; 86 | overflow: auto; 87 | background-color: #ffd; 88 | white-space: pre; 89 | border-color: var(--ct-notify); 90 | border-style: var(--ct-is-problematic); 91 | } 92 | 93 | head script:not(:empty)::before { 94 | content: "[Inline Script] "; 95 | } 96 | 97 | head style:not(:empty)::before { 98 | content: "[Inline Style] "; 99 | }/** 100 | * Blocked Title. 101 | * 102 | * These selectors are generally more complex because the Key Selector (\`title\`) 103 | * depends on the specific conditions of preceding JS--we can’t cast a wide net 104 | * and narrow it down later as we can when targeting elements directly. 105 | */ 106 | 107 | head script[src]:not([async]):not([defer]):not([type=module]) ~ title, 108 | head script:not(:empty) ~ title { 109 | display: block; 110 | border-style: var(--ct-is-affected); 111 | border-color: var(--ct-error); 112 | } 113 | 114 | head script[src]:not([async]):not([defer]):not([type=module]) ~ title::before, 115 | head script:not(:empty) ~ title::before { 116 | content: "[<title> blocked by JS] "; 117 | }/** 118 | * Blocked Scripts. 119 | * 120 | * These selectors are generally more complex because the Key Selector 121 | * (\`script\`) depends on the specific conditions of preceding CSS--we can’t cast 122 | * a wide net and narrow it down later as we can when targeting elements 123 | * directly. 124 | */ 125 | 126 | head [rel="stylesheet"]:not([media="print"]):not(.ct) ~ script, 127 | head style:not(:empty) ~ script { 128 | border-style: var(--ct-is-affected); 129 | border-color: var(--ct-warn); 130 | } 131 | 132 | head [rel="stylesheet"]:not([media="print"]):not(.ct) ~ script::before, 133 | head style:not(:empty) ~ script::before { 134 | content: "[JS blocked by CSS – " attr(src) "]"; 135 | }/** 136 | * Using both \`async\` and \`defer\` is redundant (an anti-pattern, even). Let’s 137 | * flag that. 138 | */ 139 | 140 | head script[src][src][async][defer] { 141 | display: block; 142 | border-style: var(--ct-is-problematic); 143 | border-color: var(--ct-warn); 144 | } 145 | 146 | head script[src][src][async][defer]::before { 147 | content: "[async and defer is redundant: prefer defer – " attr(src) "]"; 148 | }/** 149 | * Async and defer simply do not work on inline scripts. It won’t do any harm, 150 | * but it’s useful to know about. 151 | */ 152 | head script:not([src])[async], 153 | head script:not([src])[defer] { 154 | border-style: var(--ct-is-problematic); 155 | border-color: var(--ct-warn); 156 | } 157 | 158 | head script:not([src])[async]::before { 159 | content: "The async attribute is redundant on inline scripts" 160 | } 161 | 162 | head script:not([src])[defer]::before { 163 | content: "The defer attribute is redundant on inline scripts" 164 | }/** 165 | * Third Party blocking resources. 166 | * 167 | * Expect false-positives here… it’s a crude proxy at best. 168 | * 169 | * Selector-chaining (e.g. \`[src][src]\`) is used to bump up specificity. 170 | */ 171 | 172 | head script[src][src][src^="//"], 173 | head script[src][src][src^="http"], 174 | head [rel="stylesheet"][href^="//"], 175 | head [rel="stylesheet"][href^="http"] { 176 | border-style: var(--ct-is-problematic); 177 | border-color: var(--ct-error); 178 | } 179 | 180 | head script[src][src][src^="//"]::before, 181 | head script[src][src][src^="http"]::before { 182 | content: "[Third Party Blocking Script – " attr(src) "]"; 183 | } 184 | 185 | head [rel="stylesheet"][href^="//"]::before, 186 | head [rel="stylesheet"][href^="http"]::before { 187 | content: "[Third Party Blocking Stylesheet – " attr(href) "]"; 188 | }/** 189 | * Mid-HEAD CSP disables the Preload Scanner 190 | */ 191 | 192 | head script ~ meta[http-equiv="content-security-policy"] { 193 | border-style: var(--ct-is-problematic); 194 | border-color: var(--ct-error); 195 | } 196 | 197 | head script ~ meta[http-equiv="content-security-policy"]::before { 198 | content: "[Meta CSP defined after JS]" 199 | }/** 200 | * Charset should appear as early as possible 201 | */ 202 | head > meta[charset]:not(:nth-child(-n+5)) { 203 | border-style: var(--ct-is-problematic); 204 | border-color: var(--ct-warn); 205 | } 206 | 207 | head > meta[charset]:not(:nth-child(-n+5))::before { 208 | content: "[Charset should appear as early as possible]"; 209 | }/** 210 | * Hide all irrelevant or non-matching scripts and styles (including ct.css). 211 | * 212 | * We’re done! 213 | */ 214 | 215 | link[rel="stylesheet"][media="print"], 216 | link[rel="stylesheet"].ct, style.ct, 217 | script[async], script[defer], script[type=module] { 218 | display: none; 219 | } 220 | `; 221 | ct.classList.add('ct'); 222 | document.head.appendChild(ct); 223 | }()); 224 | -------------------------------------------------------------------------------- /snippets/check-image-sizes/Readme.md: -------------------------------------------------------------------------------- 1 | # Check image sizes 2 | 3 | ## Description 4 | 5 | List all image resources (also background images in styles) and checks if they are used correctly 6 | 7 | [More Info](https://developer.mozilla.org/en-US/docs/Web/API/PerformanceResourceTiming) 8 | 9 | ## How to use it 10 | 11 | <!-- START-HOW_TO[bookmark,console-tab,sources-tab,chromium] --> 12 | 13 | 14 | | Technique | Is Usable | 15 | | ----------- | ---------- | 16 | | [Bookmark](https://github.com/push-based/web-performance-tools/blob/main/docs/how-to-use-it-with-bookmarks) | ✔ | 17 | | [Console Tab](https://github.com/push-based/web-performance-tools/blob/main/docs/how-to-use-it-with-console-tab.md) | ✔ | 18 | | [Sources Tab](https://github.com/push-based/web-performance-tools/blob/main/docs/how-to-use-it-with-sources-tab.md) | ✔ | 19 | | [Chromium](https://github.com/push-based/web-performance-tools/blob/main/docs/how-to-use-it-with-chromium.md) | ✔ | 20 | 21 | 22 | 23 | ### Bookmark Snippet 24 | 25 | 26 | 27 | <details> 28 | 29 | <summary>Copy this code snippet into the bookmark to use it</summary> 30 | 31 | 32 | ```javascript 33 | 34 | javascript:(() => {function getImgs(sortBy) { 35 | const imgs = []; 36 | const resourceListEntries = performance.getEntriesByType("resource"); 37 | resourceListEntries.forEach(({ name, transferSize, encodedBodySize, decodedBodySize, initiatorType, }) => { 38 | if (initiatorType == "img") { 39 | imgs.push({ 40 | name, 41 | transferSize, 42 | decodedBodySize, 43 | encodedBodySize, 44 | }); 45 | } 46 | }); 47 | const imgList = imgs.sort((a, b) => { 48 | return b[sortBy] - a[sortBy]; 49 | }); 50 | return imgList; 51 | } 52 | console.table(getImgs("encodedBodySize")); 53 | })() 54 | ``` 55 | 56 | 57 | 58 | 59 | </details> 60 | 61 | 62 | 63 | ## Console Tab Snippet 64 | 65 | <details> 66 | 67 | <summary>Copy this code snippet into the DevTools console Tab to use it</summary> 68 | 69 | 70 | ```javascript 71 | 72 | function getImgs(sortBy) { 73 | const imgs = []; 74 | const resourceListEntries = performance.getEntriesByType("resource"); 75 | resourceListEntries.forEach(({ name, transferSize, encodedBodySize, decodedBodySize, initiatorType, }) => { 76 | if (initiatorType == "img") { 77 | imgs.push({ 78 | name, 79 | transferSize, 80 | decodedBodySize, 81 | encodedBodySize, 82 | }); 83 | } 84 | }); 85 | const imgList = imgs.sort((a, b) => { 86 | return b[sortBy] - a[sortBy]; 87 | }); 88 | return imgList; 89 | } 90 | console.table(getImgs("encodedBodySize")); 91 | 92 | ``` 93 | 94 | 95 | 96 | 97 | </details> 98 | 99 | 100 | 101 | 102 | <!-- END-HOW_TO --> 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | # Credits 208 | 209 | Author: _Joan León_ 210 | Source: _[github.com/nucliweb/webperf-snippets](https://github.com/nucliweb/webperf-snippets/blob/main/README.md#first-and-third-party-script-info)_ 211 | 212 | -------------------------------------------------------------------------------- /snippets/check-image-sizes/index.js: -------------------------------------------------------------------------------- 1 | function getImgs(sortBy) { 2 | const imgs = []; 3 | 4 | const resourceListEntries = performance.getEntriesByType("resource"); 5 | resourceListEntries.forEach( 6 | ({ 7 | name, 8 | transferSize, 9 | encodedBodySize, 10 | decodedBodySize, 11 | initiatorType, 12 | }) => { 13 | if (initiatorType == "img") { 14 | imgs.push({ 15 | name, 16 | transferSize, 17 | decodedBodySize, 18 | encodedBodySize, 19 | }); 20 | } 21 | } 22 | ); 23 | 24 | const imgList = imgs.sort((a, b) => { 25 | return b[sortBy] - a[sortBy]; 26 | }); 27 | 28 | return imgList; 29 | } 30 | console.table(getImgs("encodedBodySize")); 31 | -------------------------------------------------------------------------------- /snippets/check-image-srcset/Readme.md: -------------------------------------------------------------------------------- 1 | # Check image srcset 2 | 3 | ## Description 4 | This script helps to debug srcset of [responsive images](web.dev/responsive-images). 5 | It prints changes in the current src and logs the current url and clientWidth. 6 | 7 | ## How to use it 8 | 9 | ### Devtools setup 10 | - select network tab 11 | - disable cache 12 | - remove all columns but name and path 13 | - filter for images only 14 | - in the net work configuration area (cog wheel) select big icons 15 | - use the search filter to reduce the number of results 16 | - open device toolbar 17 | - in the menu select show device pixel ratio 18 | - use a flexible with 19 | 20 | ### Use source snippet 21 | - add snippet 22 | - open quick Sources panel 23 | - execute script 24 | - resize page 25 | - watch console and screen 26 | 27 | You will get a output in the console that updates while you resize the screen. 28 | 29 | It includes: 30 | - DPR 31 | - The element you debug ATM. This is good for debugging because you can just hover the element in the console and there is no need to fiddle in the elements tab 32 | - A table of breakpoints where the current `src` changed including the intrinsic size, rendered size and the ratio of the 2 33 | - A log of the data in form od a normal log below the table. This is useful if the table size does not allow to see all information 34 | 35 | <img src="./assets/srcset-script-debugging--console_michael-hladky.png" alt="srcset debugging console output" width="400"/> 36 | 37 | As a visual support the selected image will get a couple of styles set to reflect the correctness: 38 | - oversized images will get a red border where the border width is proportional to the ratio of oversize 39 | - undersized images will get a blue border where the border width is 1px and the opacity is proportional to the undersize 40 | 41 | | Oversize | Undersize | 42 | | ----------- | ---------- | 43 | | <img src="./assets/srcset-script-debugging--oversize_michael-hladky.png" alt="srcset debugging oversized images" width="200"/> | <img src="./assets/srcset-script-debugging--undersize_michael-hladky.png" alt="srcset debugging undersized images" width="200"/> | 44 | 45 | Here a small [video to see it in action](./assets/srcset-script-debugging__michael-hladky.mp4): 46 | [<img src="./assets/srcset-script-debugging__michael-hladky.gif">](./assets/srcset-script-debugging__michael-hladky.mp4) 47 | 48 | 49 | 50 | <!-- START-HOW_TO[bookmark,console-tab] --> 51 | 52 | 53 | | Technique | Is Usable | 54 | | ----------- | ---------- | 55 | | [Bookmark](https://github.com/push-based/web-performance-tools/blob/main/docs/how-to-use-it-with-bookmarks) | ✔ | 56 | | [Console Tab](https://github.com/push-based/web-performance-tools/blob/main/docs/how-to-use-it-with-console-tab.md) | ✔ | 57 | | [Sources Tab](https://github.com/push-based/web-performance-tools/blob/main/docs/how-to-use-it-with-sources-tab.md) | ❌ | 58 | | [Chromium](https://github.com/push-based/web-performance-tools/blob/main/docs/how-to-use-it-with-chromium.md) | ❌ | 59 | 60 | 61 | 62 | ### Bookmark Snippet 63 | 64 | 65 | 66 | <details> 67 | 68 | <summary>Copy this code snippet into the bookmark to use it</summary> 69 | 70 | 71 | ```javascript 72 | 73 | javascript:(() => {function checkImgSrcset(selector) { 74 | selector = selector || prompt('Img selector (e.g. div.test > img)'); 75 | let lastSrc = ''; 76 | const switches = []; 77 | const el = document.querySelector(selector); 78 | if (!el) { 79 | throw (`Could not fnd any element with selector ${selector}`); 80 | } 81 | const resizeObserver = new ResizeObserver((entries) => { 82 | const clientWidth = document.body.clientWidth; 83 | for (const entry of entries) { 84 | const img = entry.target; 85 | if (lastSrc !== img.currentSrc) { 86 | lastSrc = img.currentSrc; 87 | lastSrc && loadImg(lastSrc).then(i => { 88 | switches.push({ 89 | clientWidth, 90 | element: el, 91 | src: lastSrc, 92 | intrinsicWith: i.width, 93 | intrinsicHeight: i.height, 94 | renderedWith: el.clientWidth, 95 | renderedHeight: el.clientHeight, 96 | sizeDiff: ((i.width * i.height) / (el.clientWidth * el.clientHeight)) 97 | }); 98 | highlightElement(switches); 99 | logData(switches); 100 | }); 101 | highlightElement(switches); 102 | logData(switches); 103 | } 104 | } 105 | }); 106 | resizeObserver.observe(el); 107 | } 108 | function logData(data) { 109 | console.clear(); 110 | console.table(prepareTable(data)); 111 | } 112 | function highlightElement(arr) { 113 | arr.forEach(o => { 114 | const { element, intrinsicWith, intrinsicHeight } = o; 115 | if (element && intrinsicWith && intrinsicHeight) { 116 | const d = ((intrinsicWith * intrinsicHeight) / (element.clientWidth * element.clientHeight)); 117 | // for over-size border for under-size opacity? 118 | element.style.border = 1 + 'px solid red'; 119 | element.style.opacity = 0.5 * d; 120 | } 121 | }); 122 | } 123 | function prepareTable(arr) { 124 | return arr 125 | .map(({ element, ...inTable }) => ({ 126 | dpr: window.devicePixelRatio, 127 | clientWidth: inTable.clientWidth + 'px', 128 | src: inTable.src, 129 | intrinsicSize: inTable.intrinsicWith + 'x' + inTable.intrinsicHeight + 'px', 130 | renderedSize: inTable.renderedWith + 'x' + inTable.renderedHeight + 'px', 131 | sizeDiff: inTable.sizeDiff.toFixed(2) 132 | })); 133 | } 134 | function loadImg(url) { 135 | return new Promise((resolve, reject) => { 136 | const img = new Image; 137 | img.onload = function () { 138 | resolve(img); 139 | }; 140 | img.onerror = (e) => reject(e); 141 | img.src = url; 142 | }); 143 | } 144 | ; 145 | checkImgSrcset(); 146 | })() 147 | ``` 148 | 149 | 150 | 151 | 152 | </details> 153 | 154 | 155 | 156 | ## Console Tab Snippet 157 | 158 | <details> 159 | 160 | <summary>Copy this code snippet into the DevTools console Tab to use it</summary> 161 | 162 | 163 | ```javascript 164 | 165 | function checkImgSrcset(selector) { 166 | selector = selector || prompt('Img selector (e.g. div.test > img)'); 167 | let lastSrc = ''; 168 | const switches = []; 169 | const el = document.querySelector(selector); 170 | if (!el) { 171 | throw (`Could not fnd any element with selector ${selector}`); 172 | } 173 | const resizeObserver = new ResizeObserver((entries) => { 174 | const clientWidth = document.body.clientWidth; 175 | for (const entry of entries) { 176 | const img = entry.target; 177 | if (lastSrc !== img.currentSrc) { 178 | lastSrc = img.currentSrc; 179 | lastSrc && loadImg(lastSrc).then(i => { 180 | switches.push({ 181 | clientWidth, 182 | element: el, 183 | src: lastSrc, 184 | intrinsicWith: i.width, 185 | intrinsicHeight: i.height, 186 | renderedWith: el.clientWidth, 187 | renderedHeight: el.clientHeight, 188 | sizeDiff: ((i.width * i.height) / (el.clientWidth * el.clientHeight)) 189 | }); 190 | highlightElement(switches); 191 | logData(switches); 192 | }); 193 | highlightElement(switches); 194 | logData(switches); 195 | } 196 | } 197 | }); 198 | resizeObserver.observe(el); 199 | } 200 | function logData(data) { 201 | console.clear(); 202 | console.table(prepareTable(data)); 203 | } 204 | function highlightElement(arr) { 205 | arr.forEach(o => { 206 | const { element, intrinsicWith, intrinsicHeight } = o; 207 | if (element && intrinsicWith && intrinsicHeight) { 208 | const d = ((intrinsicWith * intrinsicHeight) / (element.clientWidth * element.clientHeight)); 209 | // for over-size border for under-size opacity? 210 | element.style.border = 1 + 'px solid red'; 211 | element.style.opacity = 0.5 * d; 212 | } 213 | }); 214 | } 215 | function prepareTable(arr) { 216 | return arr 217 | .map(({ element, ...inTable }) => ({ 218 | dpr: window.devicePixelRatio, 219 | clientWidth: inTable.clientWidth + 'px', 220 | src: inTable.src, 221 | intrinsicSize: inTable.intrinsicWith + 'x' + inTable.intrinsicHeight + 'px', 222 | renderedSize: inTable.renderedWith + 'x' + inTable.renderedHeight + 'px', 223 | sizeDiff: inTable.sizeDiff.toFixed(2) 224 | })); 225 | } 226 | function loadImg(url) { 227 | return new Promise((resolve, reject) => { 228 | const img = new Image; 229 | img.onload = function () { 230 | resolve(img); 231 | }; 232 | img.onerror = (e) => reject(e); 233 | img.src = url; 234 | }); 235 | } 236 | ; 237 | checkImgSrcset(); 238 | 239 | ``` 240 | 241 | 242 | 243 | 244 | </details> 245 | 246 | 247 | 248 | 249 | <!-- END-HOW_TO --> 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | -------------------------------------------------------------------------------- /snippets/check-image-srcset/assets/srcset-script-debugging--console_michael-hladky.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/push-based/awesome-web-performance-snippets/2273b69d62e12f44520a5d61cf8cd6c1fa6131b6/snippets/check-image-srcset/assets/srcset-script-debugging--console_michael-hladky.png -------------------------------------------------------------------------------- /snippets/check-image-srcset/assets/srcset-script-debugging--oversize_michael-hladky.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/push-based/awesome-web-performance-snippets/2273b69d62e12f44520a5d61cf8cd6c1fa6131b6/snippets/check-image-srcset/assets/srcset-script-debugging--oversize_michael-hladky.png -------------------------------------------------------------------------------- /snippets/check-image-srcset/assets/srcset-script-debugging--undersize_michael-hladky.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/push-based/awesome-web-performance-snippets/2273b69d62e12f44520a5d61cf8cd6c1fa6131b6/snippets/check-image-srcset/assets/srcset-script-debugging--undersize_michael-hladky.png -------------------------------------------------------------------------------- /snippets/check-image-srcset/assets/srcset-script-debugging__michael-hladky.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/push-based/awesome-web-performance-snippets/2273b69d62e12f44520a5d61cf8cd6c1fa6131b6/snippets/check-image-srcset/assets/srcset-script-debugging__michael-hladky.gif -------------------------------------------------------------------------------- /snippets/check-image-srcset/assets/srcset-script-debugging__michael-hladky.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/push-based/awesome-web-performance-snippets/2273b69d62e12f44520a5d61cf8cd6c1fa6131b6/snippets/check-image-srcset/assets/srcset-script-debugging__michael-hladky.mp4 -------------------------------------------------------------------------------- /snippets/check-image-srcset/index.ts: -------------------------------------------------------------------------------- 1 | function checkImgSrcset(selector?: string): void { 2 | selector = selector || prompt('Img selector (e.g. div.test > img)'); 3 | let lastSrc = ''; 4 | const switches = []; 5 | const el = document.querySelector(selector); 6 | if(!el) { 7 | throw(`Could not fnd any element with selector ${selector}`); 8 | } 9 | const resizeObserver = new ResizeObserver((entries) => { 10 | const clientWidth = document.body.clientWidth; 11 | for (const entry of entries) { 12 | const img = entry.target as HTMLImageElement; 13 | if (lastSrc !== img.currentSrc) { 14 | lastSrc = img.currentSrc; 15 | lastSrc && loadImg(lastSrc).then(i => { 16 | switches.push({ 17 | clientWidth, 18 | element: el, 19 | src: lastSrc, 20 | intrinsicWith: i.width, 21 | intrinsicHeight: i.height, 22 | renderedWith: el.clientWidth, 23 | renderedHeight: el.clientHeight, 24 | sizeDiff: ((i.width * i.height) / (el.clientWidth * el.clientHeight)) 25 | }); 26 | highlightElement(switches); 27 | logData(switches); 28 | }); 29 | highlightElement(switches); 30 | logData(switches); 31 | } 32 | } 33 | }) 34 | resizeObserver.observe(el); 35 | } 36 | 37 | function logData(data) { 38 | console.clear(); 39 | console.table(prepareTable(data)); 40 | } 41 | 42 | function highlightElement(arr) { 43 | arr.forEach(o => { 44 | const {element, intrinsicWith, intrinsicHeight} = o; 45 | if(element && intrinsicWith && intrinsicHeight) { 46 | const d = ((intrinsicWith * intrinsicHeight) / (element.clientWidth * element.clientHeight)); 47 | // for over-size border for under-size opacity? 48 | element.style.border = 1+'px solid red'; 49 | element.style.opacity = 0.5*d; 50 | } 51 | }) 52 | } 53 | 54 | 55 | function prepareTable(arr) { 56 | return arr 57 | .map(({ element, ...inTable }) => ({ 58 | dpr: window.devicePixelRatio, 59 | clientWidth: inTable.clientWidth + 'px', 60 | src: inTable.src, 61 | intrinsicSize: inTable.intrinsicWith + 'x' + inTable.intrinsicHeight + 'px', 62 | renderedSize: inTable.renderedWith + 'x' + inTable.renderedHeight + 'px', 63 | sizeDiff: inTable.sizeDiff.toFixed(2) 64 | })) 65 | } 66 | 67 | function loadImg(url): Promise<HTMLImageElement> { 68 | return new Promise((resolve, reject) => { 69 | const img = new Image; 70 | img.onload = function() { 71 | resolve(img) 72 | }; 73 | img.onerror = (e) => reject(e); 74 | img.src = url; 75 | }) 76 | }; 77 | 78 | checkImgSrcset(); -------------------------------------------------------------------------------- /snippets/check-image-usage/index.js: -------------------------------------------------------------------------------- 1 | const bgUrlChecker = /(url\(["'])([A-Za-z0-9$.:/_\-~]*)(["']\))(?!data:$)/g; 2 | const base64UrlChecker = /(url\(["'])(data:)([A-Za-z0-9$.:/_\-~]*)/g; 3 | const srcChecker = /(src=["'])([A-Za-z0-9$.:/_\-~]*)(["'])(?!data:$)/g; 4 | 5 | const bgSRule = 'background'; 6 | const bgImgSRule = 'background-image'; 7 | 8 | const msgNotLazyLoaded = "❌ not lazy loaded"; 9 | const msgNotEagerLoaded = "❌ not eager loaded"; 10 | const msgDontUseBgImage = "❌ don't use bg image"; 11 | const msgDontUseBgDataImage = "❌ don't use data:<format>"; 12 | const msgNotDisplayed = "⚠ fetched but not displayed"; 13 | const msgUnknown = "⚠ Case not implemented"; 14 | const msgOk = "🆗"; 15 | 16 | function fixUsage(imgs) { 17 | let l = ''; 18 | imgs.forEach(i => { 19 | switch (i.error) { 20 | case msgNotEagerLoaded: 21 | l = "eager"; 22 | break; 23 | case msgNotLazyLoaded: 24 | l = "lazy"; 25 | break; 26 | } 27 | l && i.tag.setAttribute('loading', l); 28 | }); 29 | } 30 | 31 | function highlightElements(imgs) { 32 | let s = ''; 33 | imgs.forEach(i => { 34 | switch (i.error) { 35 | case msgNotEagerLoaded: 36 | s = 'outline: 3px red solid;'; 37 | break; 38 | case msgNotLazyLoaded: 39 | s = 'outline: 3px red dotted;'; 40 | break; 41 | case msgDontUseBgDataImage: 42 | s = 'outline: 3px red dashed;'; 43 | break; 44 | case msgDontUseBgImage: 45 | s = 'outline: 3px red dashed;'; 46 | break; 47 | } 48 | s && i.tag.setAttribute('style', s); 49 | }); 50 | } 51 | 52 | function isInViewPort(tag) { 53 | return tag.offsetTop < window.innerHeight && 54 | tag.offsetTop > -tag.offsetHeight && 55 | tag.offsetLeft > -tag.offsetWidth && 56 | tag.offsetLeft < window.innerWidth 57 | } 58 | 59 | function styles(tag, pseudoElt) { 60 | return window.getComputedStyle(tag, pseudoElt || null); 61 | } 62 | 63 | function getImgRelevantRules(tag) { 64 | const res = { 65 | withBgImgNodes: new Map(), 66 | withBgDataImgNodes: new Map() 67 | }; 68 | 69 | let matchBgB64 = base64UrlChecker.exec(tag.attributes.src); 70 | if (matchBgB64) { 71 | res.withBgImgNodes.set(matchBgB64[3], tag); 72 | } 73 | 74 | [null, '::before', '::after'] 75 | .map((pseudoElt) => { 76 | const backgroundVal = styles(tag, pseudoElt).getPropertyValue(bgSRule); 77 | const backgroundImageVal = styles(tag, pseudoElt).getPropertyValue(bgImgSRule); 78 | 79 | let matchBg = bgUrlChecker.exec(backgroundVal) || bgUrlChecker.exec(backgroundImageVal); 80 | let matchBgB64 = base64UrlChecker.exec(backgroundVal) || base64UrlChecker.exec(backgroundImageVal); 81 | 82 | if (matchBg) { 83 | res.withBgImgNodes.set(matchBg[2], tag); 84 | } else if (matchBgB64) { 85 | res.withBgDataImgNodes.set(matchBgB64[3], tag); 86 | } 87 | }); 88 | 89 | return res; 90 | } 91 | 92 | function getNetworkImgs() { 93 | const imgs = new Map(); 94 | 95 | const resourceListEntries = performance.getEntriesByType("resource"); 96 | resourceListEntries.forEach( 97 | ({ 98 | name, 99 | transferSize, 100 | initiatorType, 101 | }) => { 102 | if (initiatorType == "img") { 103 | imgs.set(name, { 104 | name, 105 | transferSize 106 | }); 107 | } 108 | } 109 | ); 110 | 111 | return imgs; 112 | } 113 | 114 | function getImgsWithBackground(doc) { 115 | 116 | const withBgImgNames = new Set(); 117 | const withBgImgNodes = new Map(); 118 | const withBgDataImgNames = new Set(); 119 | const withBgDataImgNodes = new Map(); 120 | 121 | Array.from(doc.querySelectorAll('body *')) 122 | .forEach((tag) => { 123 | const badRules = getImgRelevantRules(tag); 124 | Array.from(badRules.withBgImgNodes.entries()).forEach(([url, _]) => { 125 | withBgImgNodes.set(url, tag); 126 | withBgImgNames.add(url); 127 | }); 128 | Array.from(badRules.withBgDataImgNodes.entries()).forEach(([url, _]) => { 129 | withBgDataImgNodes.set(url, tag); 130 | withBgDataImgNames.add(url); 131 | }); 132 | }) 133 | 134 | return {withBgImgNodes, withBgImgNames, withBgDataImgNodes, withBgDataImgNames}; 135 | } 136 | 137 | function findImagesAndLoadingAttribute(doc) { 138 | const imgs = doc.querySelectorAll('img'); 139 | 140 | const lazyLoadedAboveTheFoldNodes = new Map(); 141 | const lazyLoadedAboveTheFoldNames = new Set(); 142 | const eagerLoadedBelowTheFoldNodes = new Map(); 143 | const eagerLoadedBelowTheFoldNames = new Set(); 144 | 145 | imgs.forEach((tag) => { 146 | const inViewPort = isInViewPort(tag); 147 | const url = tag.attributes.src ? tag.attributes.src.value : null; 148 | 149 | // Ignore images without URL since they might be handled by custom javaScript lazy loading technique. 150 | if (!url) return; 151 | 152 | const isLazy = tag.attributes.loading === 'lazy'; 153 | if (isLazy && inViewPort) { 154 | lazyLoadedAboveTheFoldNodes.set(url, tag); 155 | lazyLoadedAboveTheFoldNames.add(url); 156 | } else if (!isLazy && !inViewPort) { 157 | eagerLoadedBelowTheFoldNodes.set(url, tag); 158 | eagerLoadedBelowTheFoldNames.add(url); 159 | } 160 | }); 161 | return { 162 | lazyLoadedAboveTheFoldNames, 163 | lazyLoadedAboveTheFoldNodes, 164 | eagerLoadedBelowTheFoldNames, 165 | eagerLoadedBelowTheFoldNodes 166 | }; 167 | } 168 | 169 | const { 170 | lazyLoadedAboveTheFoldNodes, 171 | lazyLoadedAboveTheFoldNames, 172 | eagerLoadedBelowTheFoldNodes, 173 | eagerLoadedBelowTheFoldNames 174 | } = findImagesAndLoadingAttribute(document); 175 | 176 | const { 177 | withBgDataImgNames, 178 | withBgDataImgNodes, 179 | withBgImgNames, 180 | withBgImgNodes 181 | } = getImgsWithBackground(document); 182 | 183 | const networkImgs = getNetworkImgs(); 184 | 185 | const allNames = Array.from(new Set([ 186 | ...lazyLoadedAboveTheFoldNames, 187 | ...eagerLoadedBelowTheFoldNames, 188 | ...withBgImgNames, 189 | ...withBgDataImgNames 190 | ] 191 | )); 192 | 193 | function enrichSizeUsage(imgData) { 194 | return Promise.all(imgData.map((i, idx) => { 195 | return new Promise((r) => { 196 | const img = new Image; 197 | const wRetain= i.tag.width; 198 | const hRetain= i.tag.height; 199 | img.onload = function() { 200 | // mutation! 201 | imgData[idx].imgDisplayDiff= `${wRetain}/${hRetain} to ${img.width}/${img.height}`; 202 | r(); 203 | }; 204 | img.onerror = r; 205 | img.src = i.url; 206 | }) 207 | })).then(() => imgData); 208 | } 209 | 210 | function enrichData() { 211 | return Array.from(allNames).map((url) => { 212 | 213 | let imgData = { 214 | tag: 'n/a', 215 | url, 216 | error: '', 217 | transferSize: '?' 218 | }; 219 | 220 | let errorDetected = true; 221 | switch (true) { 222 | case eagerLoadedBelowTheFoldNames.has(url): 223 | imgData.tag = eagerLoadedBelowTheFoldNodes.get(url); 224 | imgData.error = msgNotLazyLoaded; 225 | break; 226 | case lazyLoadedAboveTheFoldNames.has(url): 227 | imgData.tag = lazyLoadedAboveTheFoldNodes.get(url); 228 | imgData.error = msgNotEagerLoaded; 229 | break; 230 | case withBgImgNames.has(url): 231 | imgData.tag = withBgImgNodes.get(url); 232 | imgData.error = msgDontUseBgImage; 233 | break; 234 | case withBgDataImgNames.has(url): 235 | imgData.tag = withBgDataImgNodes.get(url); 236 | imgData.error = msgDontUseBgDataImage; 237 | imgData.transferSize = url.length * 1.02; 238 | break; 239 | default: 240 | errorDetected = false; 241 | } 242 | 243 | if (networkImgs.has(url)) { 244 | const {transferSize, decodedBodySize, encodedBodySize} = networkImgs.get(url); 245 | imgData = {...imgData, transferSize, decodedBodySize}; 246 | 247 | if (!errorDetected) { 248 | imgData.error = msgOk; 249 | } 250 | } 251 | 252 | return imgData; 253 | }); 254 | } 255 | 256 | const d = enrichData(); 257 | 258 | highlightElements(d); 259 | fixUsage(d); 260 | enrichSizeUsage(d).then(console.table); 261 | -------------------------------------------------------------------------------- /snippets/cls/Readme.md: -------------------------------------------------------------------------------- 1 | ## Description 2 | 3 | To find more specific information about layout shifts, you can use [PerformanceObserver](https://developer.mozilla.org/docs/Web/API/PerformanceObserver) and register to observe entries of type `layout-shift`: 4 | 5 | Print al the CLS metrics when load the page and the user interactive with the page: 6 | 7 | ```js 8 | new PerformanceObserver(entryList => { 9 | console.log(entryList.getEntries()); 10 | }).observe({ type: "layout-shift", buffered: true }); 11 | ``` 12 | 13 | ## How to use it 14 | 15 | <!-- START-HOW_TO[bookmark,console-tab,sources-tab,chromium] --> 16 | 17 | 18 | | Technique | Is Usable | 19 | | ----------- | ---------- | 20 | | [Bookmark](https://github.com/push-based/web-performance-tools/blob/main/docs/how-to-use-it-with-bookmarks) | ✔ | 21 | | [Console Tab](https://github.com/push-based/web-performance-tools/blob/main/docs/how-to-use-it-with-console-tab.md) | ✔ | 22 | | [Sources Tab](https://github.com/push-based/web-performance-tools/blob/main/docs/how-to-use-it-with-sources-tab.md) | ✔ | 23 | | [Chromium](https://github.com/push-based/web-performance-tools/blob/main/docs/how-to-use-it-with-chromium.md) | ✔ | 24 | 25 | 26 | 27 | ### Bookmark Snippet 28 | 29 | 30 | 31 | <details> 32 | 33 | <summary>Copy this code snippet into the bookmark to use it</summary> 34 | 35 | 36 | ```javascript 37 | 38 | javascript:(() => {function genColor() { 39 | let n = (Math.random() * 0xfffff * 1000000).toString(16); 40 | return "#" + n.slice(0, 6); 41 | } 42 | // console.log(shifts) to see full list of shifts above threshold 43 | const shifts = []; 44 | // threshold ex: 0.05 45 | // Layout Shifts will be grouped by color. 46 | // All nodes attributed to the shift will have a border with the corresponding color 47 | // Shift value will be added above parent node. 48 | // Will have all details related to that shift in dropdown 49 | // Useful for single page applications and finding shifts after initial load 50 | function findShifts(threshold) { 51 | return new PerformanceObserver((list) => { 52 | list.getEntries().forEach((entry) => { 53 | if (entry.value > threshold && !entry.hadRecentInput) { 54 | const color = genColor(); 55 | shifts.push(entry); 56 | console.log(shifts); 57 | const valueNode = document.createElement("details"); 58 | valueNode.innerHTML = ` 59 | <summary>Layout Shift: ${entry.value}</summary> 60 | <pre>${JSON.stringify(entry, null, 2)}</pre> 61 | `; 62 | valueNode.style = `color: ${color};`; 63 | entry.sources.forEach((source) => { 64 | source.node.parentNode.insertBefore(valueNode, source.node); 65 | source.node.style = `border: 2px ${color} solid`; 66 | }); 67 | } 68 | }); 69 | }); 70 | } 71 | findShifts(0.05).observe({ entryTypes: ["layout-shift"] }); 72 | })() 73 | ``` 74 | 75 | 76 | 77 | 78 | </details> 79 | 80 | 81 | 82 | ## Console Tab Snippet 83 | 84 | <details> 85 | 86 | <summary>Copy this code snippet into the DevTools console Tab to use it</summary> 87 | 88 | 89 | ```javascript 90 | 91 | function genColor() { 92 | let n = (Math.random() * 0xfffff * 1000000).toString(16); 93 | return "#" + n.slice(0, 6); 94 | } 95 | // console.log(shifts) to see full list of shifts above threshold 96 | const shifts = []; 97 | // threshold ex: 0.05 98 | // Layout Shifts will be grouped by color. 99 | // All nodes attributed to the shift will have a border with the corresponding color 100 | // Shift value will be added above parent node. 101 | // Will have all details related to that shift in dropdown 102 | // Useful for single page applications and finding shifts after initial load 103 | function findShifts(threshold) { 104 | return new PerformanceObserver((list) => { 105 | list.getEntries().forEach((entry) => { 106 | if (entry.value > threshold && !entry.hadRecentInput) { 107 | const color = genColor(); 108 | shifts.push(entry); 109 | console.log(shifts); 110 | const valueNode = document.createElement("details"); 111 | valueNode.innerHTML = ` 112 | <summary>Layout Shift: ${entry.value}</summary> 113 | <pre>${JSON.stringify(entry, null, 2)}</pre> 114 | `; 115 | valueNode.style = `color: ${color};`; 116 | entry.sources.forEach((source) => { 117 | source.node.parentNode.insertBefore(valueNode, source.node); 118 | source.node.style = `border: 2px ${color} solid`; 119 | }); 120 | } 121 | }); 122 | }); 123 | } 124 | findShifts(0.05).observe({ entryTypes: ["layout-shift"] }); 125 | 126 | ``` 127 | 128 | 129 | 130 | 131 | </details> 132 | 133 | 134 | 135 | 136 | <!-- END-HOW_TO --> 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | # Credits 242 | 243 | Author: _Joan León_ 244 | Source: _[github.com/nucliweb/webperf-snippets](https://github.com/nucliweb/webperf-snippets/blob/main/README.md#first-and-third-party-script-info)_ 245 | 246 | -------------------------------------------------------------------------------- /snippets/cls/index.js: -------------------------------------------------------------------------------- 1 | function genColor() { 2 | let n = (Math.random() * 0xfffff * 1000000).toString(16); 3 | return "#" + n.slice(0, 6); 4 | } 5 | 6 | // console.log(shifts) to see full list of shifts above threshold 7 | const shifts = []; 8 | 9 | // threshold ex: 0.05 10 | // Layout Shifts will be grouped by color. 11 | // All nodes attributed to the shift will have a border with the corresponding color 12 | // Shift value will be added above parent node. 13 | // Will have all details related to that shift in dropdown 14 | // Useful for single page applications and finding shifts after initial load 15 | 16 | function findShifts(threshold) { 17 | return new PerformanceObserver((list) => { 18 | list.getEntries().forEach((entry) => { 19 | if (entry.value > threshold && !entry.hadRecentInput) { 20 | const color = genColor(); 21 | shifts.push(entry); 22 | console.log(shifts); 23 | 24 | const valueNode = document.createElement("details"); 25 | valueNode.innerHTML = ` 26 | <summary>Layout Shift: ${entry.value}</summary> 27 | <pre>${JSON.stringify(entry, null, 2)}</pre> 28 | `; 29 | valueNode.style = `color: ${color};`; 30 | entry.sources.forEach((source) => { 31 | source.node.parentNode.insertBefore(valueNode, source.node); 32 | source.node.style = `border: 2px ${color} solid`; 33 | }); 34 | } 35 | }); 36 | }); 37 | } 38 | 39 | findShifts(0.05).observe({ entryTypes: ["layout-shift"] }); 40 | -------------------------------------------------------------------------------- /snippets/constants.ts: -------------------------------------------------------------------------------- 1 | import {Technique} from "./types"; 2 | 3 | export const SNIPPETS_ROOT = `./snippets`; 4 | export const SNIPPETS_TEMPLATE_NAME = `SNIPPET_TEMPLATE`; 5 | export const SNIPPETS_DIST = `./dist/snippets`; 6 | export const NEW_LINE = "\r\n"; 7 | export const HOW_TO_START_REGEX = /(<!-- *START-HOW_TO\[)([a-z\-, ]*)(\] *-->)/g; 8 | export const HOW_TO_START = (t: Technique[]) => `<!-- START-HOW_TO[${t.join(`,`)}] -->`; 9 | export const HOW_TO_END = `<!-- END-HOW_TO -->`; 10 | -------------------------------------------------------------------------------- /snippets/cumulative-layout-shift/Readme.md: -------------------------------------------------------------------------------- 1 | # Cumulative Layout Shift 2 | 3 | ## Description 4 | 5 | TODO 6 | 7 | ## How to use it 8 | 9 | <!-- START-HOW_TO[] --> 10 | 11 | 12 | 13 | 14 | ### Bookmark Snippet 15 | 16 | 17 | 18 | <details> 19 | 20 | <summary>Copy this code snippet into the bookmark to use it</summary> 21 | 22 | 23 | ```javascript 24 | 25 | javascript:(() => {try { 26 | let cumulativeLayoutShiftScore = 0; 27 | const observer = new PerformanceObserver((list) => { 28 | for (const entry of list.getEntries()) { 29 | if (!entry.hadRecentInput) { 30 | cumulativeLayoutShiftScore += entry.value; 31 | } 32 | } 33 | }); 34 | observer.observe({ type: "layout-shift", buffered: true }); 35 | document.addEventListener("visibilitychange", () => { 36 | if (document.visibilityState === "hidden") { 37 | observer.takeRecords(); 38 | observer.disconnect(); 39 | console.log(`CLS: ${cumulativeLayoutShiftScore}`); 40 | } 41 | }); 42 | } 43 | catch (e) { 44 | console.log(`Browser doesn't support this API`); 45 | } 46 | })() 47 | ``` 48 | 49 | 50 | 51 | 52 | </details> 53 | 54 | 55 | 56 | ## Console Tab Snippet 57 | 58 | <details> 59 | 60 | <summary>Copy this code snippet into the DevTools console Tab to use it</summary> 61 | 62 | 63 | ```javascript 64 | 65 | try { 66 | let cumulativeLayoutShiftScore = 0; 67 | const observer = new PerformanceObserver((list) => { 68 | for (const entry of list.getEntries()) { 69 | if (!entry.hadRecentInput) { 70 | cumulativeLayoutShiftScore += entry.value; 71 | } 72 | } 73 | }); 74 | observer.observe({ type: "layout-shift", buffered: true }); 75 | document.addEventListener("visibilitychange", () => { 76 | if (document.visibilityState === "hidden") { 77 | observer.takeRecords(); 78 | observer.disconnect(); 79 | console.log(`CLS: ${cumulativeLayoutShiftScore}`); 80 | } 81 | }); 82 | } 83 | catch (e) { 84 | console.log(`Browser doesn't support this API`); 85 | } 86 | 87 | ``` 88 | 89 | 90 | 91 | 92 | </details> 93 | 94 | 95 | 96 | 97 | <!-- END-HOW_TO --> 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | # Credits 203 | 204 | Author: _Joan León_ 205 | Source: _[github.com/nucliweb/webperf-snippets](https://github.com/nucliweb/webperf-snippets/blob/main/README.md#first-and-third-party-script-info)_ 206 | 207 | -------------------------------------------------------------------------------- /snippets/cumulative-layout-shift/index.js: -------------------------------------------------------------------------------- 1 | try { 2 | let cumulativeLayoutShiftScore = 0; 3 | const observer = new PerformanceObserver((list) => { 4 | for (const entry of list.getEntries()) { 5 | if (!entry.hadRecentInput) { 6 | cumulativeLayoutShiftScore += entry.value; 7 | } 8 | } 9 | }); 10 | 11 | observer.observe({ type: "layout-shift", buffered: true }); 12 | 13 | document.addEventListener("visibilitychange", () => { 14 | if (document.visibilityState === "hidden") { 15 | observer.takeRecords(); 16 | observer.disconnect(); 17 | 18 | console.log(`CLS: ${cumulativeLayoutShiftScore}`); 19 | } 20 | }); 21 | } catch (e) { 22 | console.log(`Browser doesn't support this API`); 23 | } 24 | -------------------------------------------------------------------------------- /snippets/full-relayout/Readme.md: -------------------------------------------------------------------------------- 1 | # Estimate CSS render costs 2 | 3 | ## Description 4 | 5 | Get's estimations for a full page re-calculate styles, layout and paint. 6 | 7 | Changing the zoom level of the page causes recalculations of styles. 8 | When running the script multiple times in a row while recording in the performance tab, 9 | you can and get rough estimations about the different parts of the DOM. 10 | 11 | ## Usage 12 | 13 | 1. Start the recording in the performance tab 14 | 2. Execute the script over one of the given options multiple times in a row with a small break inbetween 15 | 3. Stop the recording and analyze the flames 16 | 17 | ![full-page-relayout](images/full-page-relayout.png) 18 | 19 | You can use it as a base measurement to compare against global CSS improvements. 20 | 21 | ![full-page-relayout-comparison](images/full-page-relayout-comparison.png) 22 | 23 | ## How to use it 24 | 25 | <!-- START-HOW_TO[bookmark,console-tab,sources-tab,chromium] --> 26 | 27 | 28 | | Technique | Is Usable | 29 | | ----------- | ---------- | 30 | | [Bookmark](https://github.com/push-based/web-performance-tools/blob/main/docs/how-to-use-it-with-bookmarks) | ✔ | 31 | | [Console Tab](https://github.com/push-based/web-performance-tools/blob/main/docs/how-to-use-it-with-console-tab.md) | ✔ | 32 | | [Sources Tab](https://github.com/push-based/web-performance-tools/blob/main/docs/how-to-use-it-with-sources-tab.md) | ✔ | 33 | | [Chromium](https://github.com/push-based/web-performance-tools/blob/main/docs/how-to-use-it-with-chromium.md) | ✔ | 34 | 35 | 36 | 37 | ### Bookmark Snippet 38 | 39 | 40 | 41 | <details> 42 | 43 | <summary>Copy this code snippet into the bookmark to use it</summary> 44 | 45 | 46 | ```javascript 47 | 48 | javascript:(() => {const b = document.body; 49 | b.style.zoom === '1' ? b.style.zoom = '1.01' : b.style.zoom = '1'; 50 | })() 51 | ``` 52 | 53 | 54 | 55 | 56 | </details> 57 | 58 | 59 | 60 | ## Console Tab Snippet 61 | 62 | <details> 63 | 64 | <summary>Copy this code snippet into the DevTools console Tab to use it</summary> 65 | 66 | 67 | ```javascript 68 | 69 | const b = document.body; 70 | b.style.zoom === '1' ? b.style.zoom = '1.01' : b.style.zoom = '1'; 71 | 72 | ``` 73 | 74 | 75 | 76 | 77 | </details> 78 | 79 | 80 | 81 | 82 | <!-- END-HOW_TO --> 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 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | # Credits 188 | 189 | Author: _Michael Hladky - push-based.io_ 190 | Source: _[github.com/push-based/web-performance-tools](www.github.com/push-based/web-performance-tools)_ 191 | -------------------------------------------------------------------------------- /snippets/full-relayout/images/full-page-relayout-comparison.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/push-based/awesome-web-performance-snippets/2273b69d62e12f44520a5d61cf8cd6c1fa6131b6/snippets/full-relayout/images/full-page-relayout-comparison.png -------------------------------------------------------------------------------- /snippets/full-relayout/images/full-page-relayout.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/push-based/awesome-web-performance-snippets/2273b69d62e12f44520a5d61cf8cd6c1fa6131b6/snippets/full-relayout/images/full-page-relayout.png -------------------------------------------------------------------------------- /snippets/full-relayout/index.js: -------------------------------------------------------------------------------- 1 | const b = document.body; 2 | b.style.zoom === '1' ? b.style.zoom = '1.01' : b.style.zoom = '1'; 3 | -------------------------------------------------------------------------------- /snippets/gathering-styles/Readme.md: -------------------------------------------------------------------------------- 1 | ## Description 2 | 3 | Gathers all style tags of the page and aggregates them to one string. 4 | 5 | ## How to use it 6 | 7 | <!-- START-HOW_TO[] --> 8 | 9 | 10 | 11 | 12 | ### Bookmark Snippet 13 | 14 | 15 | 16 | <details> 17 | 18 | <summary>Copy this code snippet into the bookmark to use it</summary> 19 | 20 | 21 | ```javascript 22 | 23 | javascript:(() => {console.log(Array.from(document.querySelectorAll('style')) 24 | .map(a => a.innerText) 25 | .reduce((a, b) => a + b)); 26 | })() 27 | ``` 28 | 29 | 30 | 31 | 32 | </details> 33 | 34 | 35 | 36 | ## Console Tab Snippet 37 | 38 | <details> 39 | 40 | <summary>Copy this code snippet into the DevTools console Tab to use it</summary> 41 | 42 | 43 | ```javascript 44 | 45 | console.log(Array.from(document.querySelectorAll('style')) 46 | .map(a => a.innerText) 47 | .reduce((a, b) => a + b)); 48 | 49 | ``` 50 | 51 | 52 | 53 | 54 | </details> 55 | 56 | 57 | 58 | 59 | <!-- END-HOW_TO --> 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 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | -------------------------------------------------------------------------------- /snippets/gathering-styles/index.js: -------------------------------------------------------------------------------- 1 | console.log(Array.from(document.querySelectorAll('style')) 2 | .map(a => a.innerText) 3 | .reduce((a,b) => a + b)); 4 | -------------------------------------------------------------------------------- /snippets/getAttributeDirectives/Readme.md: -------------------------------------------------------------------------------- 1 | ## Description 2 | 3 | Gets information about all attribute directives in the page or specific attribute directive type 4 | 5 | ## How to use it 6 | 7 | <!-- START-HOW_TO[bookmark,console-tab,sources-tab,chromium] --> 8 | 9 | 10 | | Technique | Is Usable | 11 | | ----------- | ---------- | 12 | | [Bookmark](https://github.com/push-based/web-performance-tools/blob/main/docs/how-to-use-it-with-bookmarks) | ✔ | 13 | | [Console Tab](https://github.com/push-based/web-performance-tools/blob/main/docs/how-to-use-it-with-console-tab.md) | ✔ | 14 | | [Sources Tab](https://github.com/push-based/web-performance-tools/blob/main/docs/how-to-use-it-with-sources-tab.md) | ✔ | 15 | | [Chromium](https://github.com/push-based/web-performance-tools/blob/main/docs/how-to-use-it-with-chromium.md) | ✔ | 16 | 17 | 18 | 19 | ### Bookmark Snippet 20 | 21 | 22 | 23 | <details> 24 | 25 | <summary>Copy this code snippet into the bookmark to use it</summary> 26 | 27 | 28 | ```javascript 29 | 30 | javascript:(() => {function getAttributeDirectives() { 31 | const { name, showSummaryInDOM, appPrefixes, mode } = initializeFlow(); 32 | /** 33 | * Filter out nodes that don't have an attribute 34 | */ 35 | function filterAttribute(attribute, prefixes) { 36 | return Array.isArray(prefixes) 37 | ? prefixes.some((p) => attribute.name ? attribute.name.startsWith(p.toLowerCase()) : false) 38 | : attribute.name 39 | ? attribute.name.startsWith(prefixes.toLowerCase()) 40 | : false; 41 | } 42 | function initializeFlow() { 43 | /** 44 | * Clearing summary items from DOM. 45 | */ 46 | const summaries = document.querySelectorAll(".customSummaryItem"); 47 | summaries.forEach((i) => i.remove()); 48 | const mode = prompt("Mode: summary or directive"); 49 | switch (mode) { 50 | case "directive": 51 | return { 52 | mode, 53 | name: prompt("Directive name"), 54 | showSummaryInDOM: prompt("Apply summary info to elements? (yes/no)", "no") === "yes" 55 | ? true 56 | : false, 57 | }; 58 | case "summary": 59 | return { 60 | mode, 61 | appPrefixes: prompt("Directives prefixes, comma separated. (ex: app)") 62 | .split(",") 63 | .map((p) => p.trim()) || "app", 64 | showSummaryInDOM: prompt("Apply summary info to elements? (yes/no)", "no") === "yes" 65 | ? true 66 | : false, 67 | }; 68 | } 69 | } 70 | /** 71 | * Set of checks to determine if element is hidden. 72 | */ 73 | function isHidden(element) { 74 | return !(element.offsetWidth || 75 | element.offsetHeight || 76 | element.getClientRects().length); 77 | } 78 | // Checks if element is in viewport 79 | function isInViewport(element) { 80 | return (element.offsetTop < window.innerHeight && 81 | element.offsetTop > -element.offsetHeight && 82 | element.offsetLeft > -element.offsetWidth && 83 | element.offsetLeft < window.innerWidth); 84 | } 85 | /** 86 | * Adds summary div to references 87 | */ 88 | function addSummary(nodes, prefixes) { 89 | nodes.forEach((n) => { 90 | n.style.position = "relative"; 91 | const node = document.createElement("DIV"); 92 | Object.assign(node.style, { 93 | position: "absolute", 94 | top: "0", 95 | left: "0", 96 | "z-index": "999999", 97 | "font-size": "14px", 98 | display: "flex", 99 | background: "green", 100 | color: "#fff", 101 | padding: "4px", 102 | }); 103 | node.classList.add("customSummaryItem"); 104 | const text = document.createTextNode(`${[...n.attributes] 105 | .filter((a) => filterAttribute(a, prefixes)) 106 | .map((a) => a.name).length}`); 107 | node.appendChild(text); 108 | n.appendChild(node); 109 | }); 110 | } 111 | /** 112 | * Finds references of the nodes that contain directive with given name 113 | */ 114 | function findReferences(name) { 115 | const directives = Array.from(document.querySelectorAll(`[${name}]`)).map((r) => { 116 | return { 117 | name, 118 | hidden: isHidden(r), 119 | visible: !isHidden(r), 120 | inViewport: isInViewport(r), 121 | outOfViewport: !isInViewport(r), 122 | reference: r, 123 | }; 124 | }); 125 | return { 126 | all: directives, 127 | visible: directives.filter((c) => !c.hidden), 128 | hidden: directives.filter((c) => c.hidden), 129 | inViewport: directives.filter((c) => c.inViewport), 130 | outOfViewport: directives.filter((c) => !c.inViewport), 131 | }; 132 | } 133 | /** 134 | * Get summary data for all directives 135 | */ 136 | function getAllDirectivesSummary(prefixes) { 137 | const nodesWithDirectives = Array.from(document.body.querySelectorAll("*")).filter((e) => Array.from(e.attributes).some((a) => filterAttribute(a, prefixes))); 138 | const directives = 139 | // Find unique components names in page 140 | [ 141 | ...new Set(nodesWithDirectives 142 | .map((e) => [...e.attributes] 143 | .filter((a) => filterAttribute(a, prefixes)) 144 | .map((a) => a.name)) 145 | .reduce((acc, val) => [...acc, ...val], [])), 146 | ] 147 | .map((name) => getSpecificDirectiveSummary(name)) 148 | .reduce((acc, val) => [...acc, val[0]], []); 149 | if (showSummaryInDOM) { 150 | addSummary(nodesWithDirectives, prefixes); 151 | } 152 | return [ 153 | { 154 | name: "📝TOTAL", 155 | visible: directives 156 | .map((c) => c.visible) 157 | .reduce((acc, val) => acc + val, 0), 158 | hidden: directives 159 | .map((c) => c.hidden) 160 | .reduce((acc, val) => acc + val, 0), 161 | inViewport: directives 162 | .map((c) => c.inViewport) 163 | .reduce((acc, val) => acc + val, 0), 164 | outOfViewport: directives 165 | .map((c) => c.outOfViewport) 166 | .reduce((acc, val) => acc + val, 0), 167 | reference: "----", 168 | }, 169 | ...directives, 170 | ]; 171 | } 172 | /** 173 | * Get summary data for specific directive 174 | */ 175 | function getSpecificDirectiveSummary(name, showSummary) { 176 | const { all, visible, hidden, inViewport, outOfViewport } = findReferences(name); 177 | if (showSummary) { 178 | addSummary(all.map((e) => e.reference), name); 179 | } 180 | return [ 181 | { 182 | name: `👉 ${name}`, 183 | visible: visible.length, 184 | hidden: hidden.length, 185 | inViewport: inViewport.length, 186 | outOfViewport: outOfViewport.length, 187 | reference: { 188 | visible, 189 | hidden, 190 | inViewport, 191 | outOfViewport, 192 | }, 193 | }, 194 | ...all, 195 | ]; 196 | } 197 | switch (mode) { 198 | case "directive": 199 | return console.table(getSpecificDirectiveSummary(name, showSummaryInDOM)); 200 | case "summary": 201 | return console.table(getAllDirectivesSummary(appPrefixes)); 202 | } 203 | } 204 | })() 205 | ``` 206 | 207 | 208 | 209 | 210 | </details> 211 | 212 | 213 | 214 | ## Console Tab Snippet 215 | 216 | <details> 217 | 218 | <summary>Copy this code snippet into the DevTools console Tab to use it</summary> 219 | 220 | 221 | ```javascript 222 | 223 | function getAttributeDirectives() { 224 | const { name, showSummaryInDOM, appPrefixes, mode } = initializeFlow(); 225 | /** 226 | * Filter out nodes that don't have an attribute 227 | */ 228 | function filterAttribute(attribute, prefixes) { 229 | return Array.isArray(prefixes) 230 | ? prefixes.some((p) => attribute.name ? attribute.name.startsWith(p.toLowerCase()) : false) 231 | : attribute.name 232 | ? attribute.name.startsWith(prefixes.toLowerCase()) 233 | : false; 234 | } 235 | function initializeFlow() { 236 | /** 237 | * Clearing summary items from DOM. 238 | */ 239 | const summaries = document.querySelectorAll(".customSummaryItem"); 240 | summaries.forEach((i) => i.remove()); 241 | const mode = prompt("Mode: summary or directive"); 242 | switch (mode) { 243 | case "directive": 244 | return { 245 | mode, 246 | name: prompt("Directive name"), 247 | showSummaryInDOM: prompt("Apply summary info to elements? (yes/no)", "no") === "yes" 248 | ? true 249 | : false, 250 | }; 251 | case "summary": 252 | return { 253 | mode, 254 | appPrefixes: prompt("Directives prefixes, comma separated. (ex: app)") 255 | .split(",") 256 | .map((p) => p.trim()) || "app", 257 | showSummaryInDOM: prompt("Apply summary info to elements? (yes/no)", "no") === "yes" 258 | ? true 259 | : false, 260 | }; 261 | } 262 | } 263 | /** 264 | * Set of checks to determine if element is hidden. 265 | */ 266 | function isHidden(element) { 267 | return !(element.offsetWidth || 268 | element.offsetHeight || 269 | element.getClientRects().length); 270 | } 271 | // Checks if element is in viewport 272 | function isInViewport(element) { 273 | return (element.offsetTop < window.innerHeight && 274 | element.offsetTop > -element.offsetHeight && 275 | element.offsetLeft > -element.offsetWidth && 276 | element.offsetLeft < window.innerWidth); 277 | } 278 | /** 279 | * Adds summary div to references 280 | */ 281 | function addSummary(nodes, prefixes) { 282 | nodes.forEach((n) => { 283 | n.style.position = "relative"; 284 | const node = document.createElement("DIV"); 285 | Object.assign(node.style, { 286 | position: "absolute", 287 | top: "0", 288 | left: "0", 289 | "z-index": "999999", 290 | "font-size": "14px", 291 | display: "flex", 292 | background: "green", 293 | color: "#fff", 294 | padding: "4px", 295 | }); 296 | node.classList.add("customSummaryItem"); 297 | const text = document.createTextNode(`${[...n.attributes] 298 | .filter((a) => filterAttribute(a, prefixes)) 299 | .map((a) => a.name).length}`); 300 | node.appendChild(text); 301 | n.appendChild(node); 302 | }); 303 | } 304 | /** 305 | * Finds references of the nodes that contain directive with given name 306 | */ 307 | function findReferences(name) { 308 | const directives = Array.from(document.querySelectorAll(`[${name}]`)).map((r) => { 309 | return { 310 | name, 311 | hidden: isHidden(r), 312 | visible: !isHidden(r), 313 | inViewport: isInViewport(r), 314 | outOfViewport: !isInViewport(r), 315 | reference: r, 316 | }; 317 | }); 318 | return { 319 | all: directives, 320 | visible: directives.filter((c) => !c.hidden), 321 | hidden: directives.filter((c) => c.hidden), 322 | inViewport: directives.filter((c) => c.inViewport), 323 | outOfViewport: directives.filter((c) => !c.inViewport), 324 | }; 325 | } 326 | /** 327 | * Get summary data for all directives 328 | */ 329 | function getAllDirectivesSummary(prefixes) { 330 | const nodesWithDirectives = Array.from(document.body.querySelectorAll("*")).filter((e) => Array.from(e.attributes).some((a) => filterAttribute(a, prefixes))); 331 | const directives = 332 | // Find unique components names in page 333 | [ 334 | ...new Set(nodesWithDirectives 335 | .map((e) => [...e.attributes] 336 | .filter((a) => filterAttribute(a, prefixes)) 337 | .map((a) => a.name)) 338 | .reduce((acc, val) => [...acc, ...val], [])), 339 | ] 340 | .map((name) => getSpecificDirectiveSummary(name)) 341 | .reduce((acc, val) => [...acc, val[0]], []); 342 | if (showSummaryInDOM) { 343 | addSummary(nodesWithDirectives, prefixes); 344 | } 345 | return [ 346 | { 347 | name: "📝TOTAL", 348 | visible: directives 349 | .map((c) => c.visible) 350 | .reduce((acc, val) => acc + val, 0), 351 | hidden: directives 352 | .map((c) => c.hidden) 353 | .reduce((acc, val) => acc + val, 0), 354 | inViewport: directives 355 | .map((c) => c.inViewport) 356 | .reduce((acc, val) => acc + val, 0), 357 | outOfViewport: directives 358 | .map((c) => c.outOfViewport) 359 | .reduce((acc, val) => acc + val, 0), 360 | reference: "----", 361 | }, 362 | ...directives, 363 | ]; 364 | } 365 | /** 366 | * Get summary data for specific directive 367 | */ 368 | function getSpecificDirectiveSummary(name, showSummary) { 369 | const { all, visible, hidden, inViewport, outOfViewport } = findReferences(name); 370 | if (showSummary) { 371 | addSummary(all.map((e) => e.reference), name); 372 | } 373 | return [ 374 | { 375 | name: `👉 ${name}`, 376 | visible: visible.length, 377 | hidden: hidden.length, 378 | inViewport: inViewport.length, 379 | outOfViewport: outOfViewport.length, 380 | reference: { 381 | visible, 382 | hidden, 383 | inViewport, 384 | outOfViewport, 385 | }, 386 | }, 387 | ...all, 388 | ]; 389 | } 390 | switch (mode) { 391 | case "directive": 392 | return console.table(getSpecificDirectiveSummary(name, showSummaryInDOM)); 393 | case "summary": 394 | return console.table(getAllDirectivesSummary(appPrefixes)); 395 | } 396 | } 397 | 398 | ``` 399 | 400 | 401 | 402 | 403 | </details> 404 | 405 | 406 | 407 | 408 | <!-- END-HOW_TO --> 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 419 | 420 | 421 | 422 | 423 | 424 | 425 | 426 | 427 | 428 | 429 | 430 | 431 | 432 | 433 | 434 | 435 | 436 | 437 | 438 | 439 | 440 | 441 | 442 | 443 | 444 | 445 | 446 | 447 | 448 | 449 | 450 | 451 | 452 | 453 | 454 | 455 | 456 | 457 | 458 | 459 | 460 | 461 | 462 | 463 | 464 | 465 | 466 | 467 | 468 | 469 | 470 | 471 | 472 | 473 | 474 | 475 | 476 | 477 | 478 | 479 | 480 | 481 | 482 | 483 | 484 | 485 | 486 | 487 | 488 | 489 | 490 | 491 | 492 | 493 | 494 | 495 | 496 | 497 | 498 | 499 | 500 | 501 | 502 | 503 | 504 | 505 | 506 | 507 | 508 | 509 | 510 | 511 | 512 | 513 | ## Input 514 | 515 | - Mode: summary/component 516 | - For summary 517 | - Prefixes, separated with comma (ex: mat,cdk,app etc) 518 | - Apply summary to DOM (yes/no) 519 | - For component 520 | - Directive name 521 | - Apply summary to DOM (yes/no) 522 | 523 | ## Features 524 | 525 | - Provides summary for all directive instances and DOM on the page. 526 | - Provides summary for specific directive type 527 | - Optionally applies summary element with amount of directives to an element. 528 | -------------------------------------------------------------------------------- /snippets/getAttributeDirectives/index.js: -------------------------------------------------------------------------------- 1 | function getAttributeDirectives() { 2 | const { name, showSummaryInDOM, appPrefixes, mode } = initializeFlow(); 3 | 4 | /** 5 | * Filter out nodes that don't have an attribute 6 | */ 7 | function filterAttribute(attribute, prefixes) { 8 | return Array.isArray(prefixes) 9 | ? prefixes.some((p) => 10 | attribute.name ? attribute.name.startsWith(p.toLowerCase()) : false 11 | ) 12 | : attribute.name 13 | ? attribute.name.startsWith(prefixes.toLowerCase()) 14 | : false; 15 | } 16 | 17 | function initializeFlow() { 18 | /** 19 | * Clearing summary items from DOM. 20 | */ 21 | const summaries = document.querySelectorAll(".customSummaryItem"); 22 | summaries.forEach((i) => i.remove()); 23 | 24 | const mode = prompt("Mode: summary or directive"); 25 | 26 | switch (mode) { 27 | case "directive": 28 | return { 29 | mode, 30 | name: prompt("Directive name"), 31 | showSummaryInDOM: 32 | prompt("Apply summary info to elements? (yes/no)", "no") === "yes" 33 | ? true 34 | : false, 35 | }; 36 | case "summary": 37 | return { 38 | mode, 39 | appPrefixes: 40 | prompt("Directives prefixes, comma separated. (ex: app)") 41 | .split(",") 42 | .map((p) => p.trim()) || "app", 43 | showSummaryInDOM: 44 | prompt("Apply summary info to elements? (yes/no)", "no") === "yes" 45 | ? true 46 | : false, 47 | }; 48 | } 49 | } 50 | 51 | /** 52 | * Set of checks to determine if element is hidden. 53 | */ 54 | function isHidden(element) { 55 | return !( 56 | element.offsetWidth || 57 | element.offsetHeight || 58 | element.getClientRects().length 59 | ); 60 | } 61 | 62 | // Checks if element is in viewport 63 | function isInViewport(element) { 64 | return ( 65 | element.offsetTop < window.innerHeight && 66 | element.offsetTop > -element.offsetHeight && 67 | element.offsetLeft > -element.offsetWidth && 68 | element.offsetLeft < window.innerWidth 69 | ); 70 | } 71 | /** 72 | * Adds summary div to references 73 | */ 74 | function addSummary(nodes, prefixes) { 75 | nodes.forEach((n) => { 76 | n.style.position = "relative"; 77 | 78 | const node = document.createElement("DIV"); 79 | Object.assign(node.style, { 80 | position: "absolute", 81 | top: "0", 82 | left: "0", 83 | "z-index": "999999", 84 | "font-size": "14px", 85 | display: "flex", 86 | background: "green", 87 | color: "#fff", 88 | padding: "4px", 89 | }); 90 | node.classList.add("customSummaryItem"); 91 | 92 | const text = document.createTextNode( 93 | `${ 94 | [...n.attributes] 95 | .filter((a) => filterAttribute(a, prefixes)) 96 | .map((a) => a.name).length 97 | }` 98 | ); 99 | 100 | node.appendChild(text); 101 | 102 | n.appendChild(node); 103 | }); 104 | } 105 | /** 106 | * Finds references of the nodes that contain directive with given name 107 | */ 108 | function findReferences(name) { 109 | const directives = Array.from(document.querySelectorAll(`[${name}]`)).map( 110 | (r) => { 111 | return { 112 | name, 113 | hidden: isHidden(r), 114 | visible: !isHidden(r), 115 | inViewport: isInViewport(r), 116 | outOfViewport: !isInViewport(r), 117 | reference: r, 118 | }; 119 | } 120 | ); 121 | 122 | return { 123 | all: directives, 124 | visible: directives.filter((c) => !c.hidden), 125 | hidden: directives.filter((c) => c.hidden), 126 | inViewport: directives.filter((c) => c.inViewport), 127 | outOfViewport: directives.filter((c) => !c.inViewport), 128 | }; 129 | } 130 | /** 131 | * Get summary data for all directives 132 | */ 133 | function getAllDirectivesSummary(prefixes) { 134 | const nodesWithDirectives = Array.from( 135 | document.body.querySelectorAll("*") 136 | ).filter((e) => 137 | Array.from(e.attributes).some((a) => filterAttribute(a, prefixes)) 138 | ); 139 | const directives = 140 | // Find unique components names in page 141 | [ 142 | ...new Set( 143 | nodesWithDirectives 144 | .map((e) => 145 | [...e.attributes] 146 | .filter((a) => filterAttribute(a, prefixes)) 147 | .map((a) => a.name) 148 | ) 149 | .reduce((acc, val) => [...acc, ...val], []) 150 | ), 151 | ] 152 | .map((name) => getSpecificDirectiveSummary(name)) 153 | .reduce((acc, val) => [...acc, val[0]], []); 154 | 155 | if (showSummaryInDOM) { 156 | addSummary(nodesWithDirectives, prefixes); 157 | } 158 | 159 | return [ 160 | { 161 | name: "📝TOTAL", 162 | visible: directives 163 | .map((c) => c.visible) 164 | .reduce((acc, val) => acc + val, 0), 165 | hidden: directives 166 | .map((c) => c.hidden) 167 | .reduce((acc, val) => acc + val, 0), 168 | inViewport: directives 169 | .map((c) => c.inViewport) 170 | .reduce((acc, val) => acc + val, 0), 171 | outOfViewport: directives 172 | .map((c) => c.outOfViewport) 173 | .reduce((acc, val) => acc + val, 0), 174 | reference: "----", 175 | }, 176 | ...directives, 177 | ]; 178 | } 179 | /** 180 | * Get summary data for specific directive 181 | */ 182 | function getSpecificDirectiveSummary(name, showSummary) { 183 | const { all, visible, hidden, inViewport, outOfViewport } = findReferences( 184 | name 185 | ); 186 | 187 | if (showSummary) { 188 | addSummary( 189 | all.map((e) => e.reference), 190 | name 191 | ); 192 | } 193 | 194 | return [ 195 | { 196 | name: `👉 ${name}`, 197 | visible: visible.length, 198 | hidden: hidden.length, 199 | inViewport: inViewport.length, 200 | outOfViewport: outOfViewport.length, 201 | reference: { 202 | visible, 203 | hidden, 204 | inViewport, 205 | outOfViewport, 206 | }, 207 | }, 208 | ...all, 209 | ]; 210 | } 211 | 212 | switch (mode) { 213 | case "directive": 214 | return console.table(getSpecificDirectiveSummary(name, showSummaryInDOM)); 215 | case "summary": 216 | return console.table(getAllDirectivesSummary(appPrefixes)); 217 | } 218 | } 219 | -------------------------------------------------------------------------------- /snippets/getComponentsNodes/index.js: -------------------------------------------------------------------------------- 1 | function index() { 2 | const { 3 | name, 4 | showSummaryInDOM, 5 | appPrefixes, 6 | mode, 7 | allNodes, 8 | visibleNodes, 9 | hiddenNodes, 10 | inViewportNodes, 11 | outOfViewportNodes, 12 | } = initializeFlow(); 13 | /** 14 | * Flow init 15 | */ 16 | function initializeFlow() { 17 | /** 18 | * Clearing summary items from DOM. 19 | */ 20 | const summaries = document.querySelectorAll(".customSummaryItem"); 21 | summaries.forEach((i) => i.remove()); 22 | 23 | const mode = prompt("Mode: summary or component"); 24 | const allNodes = Array.from(document.body.querySelectorAll("*")); 25 | const visibleNodes = []; 26 | const hiddenNodes = []; 27 | const inViewportNodes = []; 28 | const outOfViewportNodes = []; 29 | 30 | allNodes.forEach((n) => { 31 | if (isHidden(n)) { 32 | hiddenNodes.push(n); 33 | } else { 34 | visibleNodes.push(n); 35 | } 36 | 37 | if (isInViewport(n)) { 38 | inViewportNodes.push(n); 39 | } else { 40 | outOfViewportNodes.push(n); 41 | } 42 | }); 43 | switch (mode) { 44 | case "component": 45 | return { 46 | mode, 47 | name: prompt("Component name"), 48 | showSummaryInDOM: 49 | prompt("Apply summary info to elements? (yes/no)", "no") === "yes" 50 | ? true 51 | : false, 52 | allNodes, 53 | visibleNodes, 54 | hiddenNodes, 55 | inViewportNodes, 56 | outOfViewportNodes, 57 | }; 58 | case "summary": 59 | return { 60 | mode, 61 | appPrefixes: 62 | prompt("Components prefixes, comma separated. (ex: app)") 63 | .split(",") 64 | .map((p) => p.trim()) || "app", 65 | showSummaryInDOM: 66 | prompt("Apply summary info to elements? (yes/no)", "no") === "yes" 67 | ? true 68 | : false, 69 | allNodes, 70 | visibleNodes, 71 | hiddenNodes, 72 | inViewportNodes, 73 | outOfViewportNodes, 74 | }; 75 | } 76 | } 77 | /** 78 | * Set of checks to determine if element is hidden. 79 | */ 80 | function isHidden(element) { 81 | return !( 82 | element.offsetWidth || 83 | element.offsetHeight || 84 | element.getClientRects().length 85 | ); 86 | } 87 | /** 88 | * Checks if element is in viewport. 89 | */ 90 | function isInViewport(element) { 91 | return ( 92 | element.offsetTop < window.innerHeight && 93 | element.offsetTop > -element.offsetHeight && 94 | element.offsetLeft > -element.offsetWidth && 95 | element.offsetLeft < window.innerWidth 96 | ); 97 | } 98 | /** 99 | * Adds summary div to references 100 | */ 101 | function addSummary(nodes) { 102 | nodes.forEach((n) => { 103 | n.references.self.style.position = "relative"; 104 | 105 | const node = document.createElement("DIV"); 106 | const totalNode = document.createElement("SPAN"); 107 | const visibleNode = document.createElement("SPAN"); 108 | const hiddenNode = document.createElement("SPAN"); 109 | 110 | const totalText = document.createTextNode( 111 | ` Total: ${n.visibleNodes + n.hiddenNodes} ` 112 | ); 113 | const visibleText = document.createTextNode( 114 | ` Visible: ${n.visibleNodes} ` 115 | ); 116 | const hiddenText = document.createTextNode(` Hidden: ${n.hiddenNodes} `); 117 | 118 | /** 119 | * Appending styles 120 | */ 121 | Object.assign(node.style, { 122 | position: "absolute", 123 | top: "0", 124 | left: "0", 125 | "z-index": "999999", 126 | "font-size": "14px", 127 | display: "flex", 128 | }); 129 | Object.assign(totalNode.style, { background: "black", color: "#fff" }); 130 | Object.assign(visibleNode.style, { background: "green", color: "#fff" }); 131 | Object.assign(hiddenNode.style, { background: "red", color: "#fff" }); 132 | 133 | totalNode.appendChild(totalText); 134 | visibleNode.appendChild(visibleText); 135 | hiddenNode.appendChild(hiddenText); 136 | 137 | node.appendChild(totalNode); 138 | node.appendChild(visibleNode); 139 | node.appendChild(hiddenNode); 140 | 141 | node.classList.add("customSummaryItem"); 142 | 143 | n.references.self.appendChild(node); 144 | }); 145 | } 146 | /** 147 | * Finds references of the component with given name 148 | */ 149 | function findReferences(name, showSummary) { 150 | const components = Array.from(document.querySelectorAll(name)).map((r) => { 151 | const childNodes = [r, ...r.querySelectorAll("*")]; 152 | const hiddenNodes = []; 153 | const visibleNodes = []; 154 | const inViewportNodes = []; 155 | const outOfViewportNodes = []; 156 | 157 | childNodes.forEach((c) => { 158 | if (isHidden(c)) { 159 | hiddenNodes.push(c); 160 | } else { 161 | visibleNodes.push(c); 162 | } 163 | 164 | if (isInViewport(c)) { 165 | inViewportNodes.push(c); 166 | } else { 167 | outOfViewportNodes.push(c); 168 | } 169 | }); 170 | 171 | return { 172 | name: r.nodeName, 173 | nodes: childNodes.length, 174 | visibleNodes: visibleNodes.length, 175 | hiddenNodes: hiddenNodes.length, 176 | inViewportNodes: inViewportNodes.length, 177 | outOfViewportNodes: outOfViewportNodes.length, 178 | hidden: isHidden(r), 179 | visible: !isHidden(r), 180 | inViewport: isInViewport(r), 181 | outOfViewport: !isInViewport(r), 182 | references: { 183 | self: r, 184 | visibleNodes, 185 | hiddenNodes, 186 | inViewportNodes, 187 | outOfViewportNodes, 188 | }, 189 | }; 190 | }); 191 | 192 | if (showSummary) { 193 | addSummary(components); 194 | } 195 | 196 | return { 197 | all: components, 198 | visible: components.filter((c) => !c.hidden), 199 | hidden: components.filter((c) => c.hidden), 200 | inViewport: components.filter((c) => c.inViewport), 201 | outOfViewport: components.filter((c) => !c.inViewport), 202 | }; 203 | } 204 | /** 205 | * Get summary data for all components 206 | */ 207 | function getAllComponentsSummary(prefixes) { 208 | const components = [ 209 | ...new Set( 210 | allNodes 211 | .filter((e) => 212 | Array.isArray(prefixes) 213 | ? prefixes.some((p) => e.nodeName.startsWith(p.toUpperCase())) 214 | : e.nodeName.startsWith(prefix.toUpperCase()) 215 | ) 216 | .map((e) => e.nodeName) 217 | ), 218 | ] 219 | .map((name) => getSpecificComponentSummary(name)) 220 | .reduce((acc, val) => [...acc, val[0]], []); 221 | 222 | return [ 223 | { 224 | name: "📝TOTAL", 225 | visible: components 226 | .map((c) => c.visible) 227 | .reduce((acc, val) => acc + val, 0), 228 | hidden: components 229 | .map((c) => c.hidden) 230 | .reduce((acc, val) => acc + val, 0), 231 | inViewport: components 232 | .map((c) => c.inViewport) 233 | .reduce((acc, val) => acc + val, 0), 234 | outOfViewport: components 235 | .map((c) => c.outOfViewport) 236 | .reduce((acc, val) => acc + val, 0), 237 | nodes: allNodes.length, 238 | visibleNodes: visibleNodes.length, 239 | hiddenNodes: hiddenNodes.length, 240 | inViewportNodes: inViewportNodes.length, 241 | outOfViewportNodes: outOfViewportNodes.length, 242 | references: "----", 243 | }, 244 | ...components, 245 | ]; 246 | } 247 | /** 248 | * Get summary data for provided component name 249 | */ 250 | function getSpecificComponentSummary(name) { 251 | const { all, visible, hidden, inViewport, outOfViewport } = findReferences( 252 | name.toUpperCase(), 253 | showSummaryInDOM 254 | ); 255 | 256 | return [ 257 | { 258 | name: `👉 ${name.toUpperCase()}`, 259 | // Components counters 260 | visible: visible.length, 261 | hidden: hidden.length, 262 | inViewport: inViewport.length, 263 | outOfViewport: outOfViewport.length, 264 | // Nodes counters 265 | nodes: all.map((r) => r.nodes).reduce((acc, val) => acc + val, 0), 266 | visibleNodes: all 267 | .map((r) => (!r.hidden ? r.visibleNodes : 0)) 268 | .reduce((acc, val) => acc + val, 0), 269 | hiddenNodes: all 270 | .map((r) => (r.hidden ? r.nodes : r.hiddenNodes)) 271 | .reduce((acc, val) => acc + val, 0), 272 | inViewportNodes: all 273 | .map((r) => r.inViewportNodes) 274 | .reduce((acc, val) => acc + val, 0), 275 | outOfViewportNodes: all 276 | .map((r) => r.outOfViewportNodes) 277 | .reduce((acc, val) => acc + val, 0), 278 | // References 279 | references: { 280 | visible, 281 | hidden, 282 | inViewport, 283 | outOfViewport, 284 | }, 285 | }, 286 | ...all, 287 | ]; 288 | } 289 | 290 | switch (mode) { 291 | case "component": 292 | return console.table(getSpecificComponentSummary(name)); 293 | case "summary": 294 | return console.table(getAllComponentsSummary(appPrefixes)); 295 | } 296 | } 297 | -------------------------------------------------------------------------------- /snippets/getDOMEventListeners/Readme.md: -------------------------------------------------------------------------------- 1 | ## Description 2 | 3 | Gets information about event lieteners on the page. 4 | 5 | Gets information about event lieteners on the page. 6 | IMPORTANT: This script can not be run as a bookmarklet because it relies on console api getEventListeners(); 7 | ## How to use it 8 | 9 | <!-- START-HOW_TO[bookmark,console-tab,sources-tab,chromium] --> 10 | 11 | 12 | | Technique | Is Usable | 13 | | ----------- | ---------- | 14 | | [Bookmark](https://github.com/push-based/web-performance-tools/blob/main/docs/how-to-use-it-with-bookmarks) | ✔ | 15 | | [Console Tab](https://github.com/push-based/web-performance-tools/blob/main/docs/how-to-use-it-with-console-tab.md) | ✔ | 16 | | [Sources Tab](https://github.com/push-based/web-performance-tools/blob/main/docs/how-to-use-it-with-sources-tab.md) | ✔ | 17 | | [Chromium](https://github.com/push-based/web-performance-tools/blob/main/docs/how-to-use-it-with-chromium.md) | ✔ | 18 | 19 | 20 | 21 | ### Bookmark Snippet 22 | 23 | 24 | 25 | <details> 26 | 27 | <summary>Copy this code snippet into the bookmark to use it</summary> 28 | 29 | 30 | ```javascript 31 | 32 | javascript:(() => {function getDOMEventListeners() { 33 | // Get all elements with event listeners 34 | const elements = [document, ...document.querySelectorAll("*")] 35 | .map((e) => { 36 | const elementListeners = window.getEventListeners(e); 37 | return { 38 | element: e, 39 | listeners: Object.keys(elementListeners) 40 | .map((key) => ({ 41 | [key]: elementListeners[key], 42 | })) 43 | .reduce((acc, listener) => ({ 44 | ...acc, 45 | ...listener, 46 | }), {}), 47 | }; 48 | }) 49 | .filter((el) => Object.keys(el.listeners).length); 50 | // Find unique listeners names 51 | const names = new Set(elements 52 | .map((e) => Object.keys(e.listeners)) 53 | .reduce((acc, listener) => [...acc, ...listener], [])); 54 | // Form output table 55 | const table = [...names].reduce((acc, n) => { 56 | const withListener = elements.filter((e) => e.listeners[n]); 57 | const total = withListener.reduce((acc, e) => acc + e.listeners[n].length, 0); 58 | const activeListeners = withListener.reduce((acc, e) => acc + e.listeners[n].filter((l) => !l.passive).length, 0); 59 | const activeReferences = withListener.reduce((acc, e) => e.listeners[n].filter((l) => !l.passive).length ? [...acc, e] : acc, []); 60 | const passiveListeners = withListener.reduce((acc, e) => acc + e.listeners[n].filter((l) => l.passive).length, 0); 61 | const passiveReferences = withListener.reduce((acc, e) => e.listeners[n].filter((l) => l.passive).length ? [...acc, e] : acc, []); 62 | const onceListeners = withListener.reduce((acc, e) => acc + e.listeners[n].filter((l) => l.once).length, 0); 63 | const onceReferences = withListener.reduce((acc, e) => e.listeners[n].filter((l) => l.once).length ? [...acc, e] : acc, []); 64 | return [ 65 | ...acc, 66 | { 67 | name: n, 68 | total, 69 | activeListeners, 70 | activeListeners, 71 | passiveListeners, 72 | onceListeners, 73 | references: { 74 | active: activeReferences, 75 | passive: passiveReferences, 76 | once: onceReferences, 77 | }, 78 | }, 79 | ]; 80 | }, []); 81 | console.table([ 82 | { 83 | name: "📝TOTAL", 84 | total: table.reduce((acc, val) => acc + val.total, 0), 85 | activeListeners: table.reduce((acc, val) => acc + val.activeListeners, 0), 86 | passiveListeners: table.reduce((acc, val) => acc + val.passiveListeners, 0), 87 | onceListeners: table.reduce((acc, val) => acc + val.onceListeners, 0), 88 | references: "----", 89 | }, 90 | ...table, 91 | ]); 92 | } 93 | })() 94 | ``` 95 | 96 | 97 | 98 | 99 | </details> 100 | 101 | 102 | 103 | ## Console Tab Snippet 104 | 105 | <details> 106 | 107 | <summary>Copy this code snippet into the DevTools console Tab to use it</summary> 108 | 109 | 110 | ```javascript 111 | 112 | function getDOMEventListeners() { 113 | // Get all elements with event listeners 114 | const elements = [document, ...document.querySelectorAll("*")] 115 | .map((e) => { 116 | const elementListeners = window.getEventListeners(e); 117 | return { 118 | element: e, 119 | listeners: Object.keys(elementListeners) 120 | .map((key) => ({ 121 | [key]: elementListeners[key], 122 | })) 123 | .reduce((acc, listener) => ({ 124 | ...acc, 125 | ...listener, 126 | }), {}), 127 | }; 128 | }) 129 | .filter((el) => Object.keys(el.listeners).length); 130 | // Find unique listeners names 131 | const names = new Set(elements 132 | .map((e) => Object.keys(e.listeners)) 133 | .reduce((acc, listener) => [...acc, ...listener], [])); 134 | // Form output table 135 | const table = [...names].reduce((acc, n) => { 136 | const withListener = elements.filter((e) => e.listeners[n]); 137 | const total = withListener.reduce((acc, e) => acc + e.listeners[n].length, 0); 138 | const activeListeners = withListener.reduce((acc, e) => acc + e.listeners[n].filter((l) => !l.passive).length, 0); 139 | const activeReferences = withListener.reduce((acc, e) => e.listeners[n].filter((l) => !l.passive).length ? [...acc, e] : acc, []); 140 | const passiveListeners = withListener.reduce((acc, e) => acc + e.listeners[n].filter((l) => l.passive).length, 0); 141 | const passiveReferences = withListener.reduce((acc, e) => e.listeners[n].filter((l) => l.passive).length ? [...acc, e] : acc, []); 142 | const onceListeners = withListener.reduce((acc, e) => acc + e.listeners[n].filter((l) => l.once).length, 0); 143 | const onceReferences = withListener.reduce((acc, e) => e.listeners[n].filter((l) => l.once).length ? [...acc, e] : acc, []); 144 | return [ 145 | ...acc, 146 | { 147 | name: n, 148 | total, 149 | activeListeners, 150 | activeListeners, 151 | passiveListeners, 152 | onceListeners, 153 | references: { 154 | active: activeReferences, 155 | passive: passiveReferences, 156 | once: onceReferences, 157 | }, 158 | }, 159 | ]; 160 | }, []); 161 | console.table([ 162 | { 163 | name: "📝TOTAL", 164 | total: table.reduce((acc, val) => acc + val.total, 0), 165 | activeListeners: table.reduce((acc, val) => acc + val.activeListeners, 0), 166 | passiveListeners: table.reduce((acc, val) => acc + val.passiveListeners, 0), 167 | onceListeners: table.reduce((acc, val) => acc + val.onceListeners, 0), 168 | references: "----", 169 | }, 170 | ...table, 171 | ]); 172 | } 173 | 174 | ``` 175 | 176 | 177 | 178 | 179 | </details> 180 | 181 | 182 | 183 | 184 | <!-- END-HOW_TO --> 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | ## Input 290 | 291 | N/A 292 | 293 | ## Features 294 | 295 | - Provides summary information about all event listeners on the page. 296 | -------------------------------------------------------------------------------- /snippets/getDOMEventListeners/index.js: -------------------------------------------------------------------------------- 1 | function getDOMEventListeners() { 2 | // Get all elements with event listeners 3 | const elements = [document, ...document.querySelectorAll("*")] 4 | .map((e) => { 5 | const elementListeners = window.getEventListeners(e); 6 | return { 7 | element: e, 8 | listeners: Object.keys(elementListeners) 9 | .map((key) => ({ 10 | [key]: elementListeners[key], 11 | })) 12 | .reduce( 13 | (acc, listener) => ({ 14 | ...acc, 15 | ...listener, 16 | }), 17 | {} 18 | ), 19 | }; 20 | }) 21 | .filter((el) => Object.keys(el.listeners).length); 22 | 23 | // Find unique listeners names 24 | const names = new Set( 25 | elements 26 | .map((e) => Object.keys(e.listeners)) 27 | .reduce((acc, listener) => [...acc, ...listener], []) 28 | ); 29 | 30 | // Form output table 31 | const table = [...names].reduce((acc, n) => { 32 | const withListener = elements.filter((e) => e.listeners[n]); 33 | const total = withListener.reduce( 34 | (acc, e) => acc + e.listeners[n].length, 35 | 0 36 | ); 37 | const activeListeners = withListener.reduce( 38 | (acc, e) => acc + e.listeners[n].filter((l) => !l.passive).length, 39 | 0 40 | ); 41 | const activeReferences = withListener.reduce( 42 | (acc, e) => 43 | e.listeners[n].filter((l) => !l.passive).length ? [...acc, e] : acc, 44 | [] 45 | ); 46 | const passiveListeners = withListener.reduce( 47 | (acc, e) => acc + e.listeners[n].filter((l) => l.passive).length, 48 | 0 49 | ); 50 | const passiveReferences = withListener.reduce( 51 | (acc, e) => 52 | e.listeners[n].filter((l) => l.passive).length ? [...acc, e] : acc, 53 | [] 54 | ); 55 | const onceListeners = withListener.reduce( 56 | (acc, e) => acc + e.listeners[n].filter((l) => l.once).length, 57 | 0 58 | ); 59 | const onceReferences = withListener.reduce( 60 | (acc, e) => 61 | e.listeners[n].filter((l) => l.once).length ? [...acc, e] : acc, 62 | [] 63 | ); 64 | 65 | return [ 66 | ...acc, 67 | { 68 | name: n, 69 | total, 70 | activeListeners, 71 | activeListeners, 72 | passiveListeners, 73 | onceListeners, 74 | references: { 75 | active: activeReferences, 76 | passive: passiveReferences, 77 | once: onceReferences, 78 | }, 79 | }, 80 | ]; 81 | }, []); 82 | 83 | console.table([ 84 | { 85 | name: "📝TOTAL", 86 | total: table.reduce((acc, val) => acc + val.total, 0), 87 | activeListeners: table.reduce((acc, val) => acc + val.activeListeners, 0), 88 | passiveListeners: table.reduce( 89 | (acc, val) => acc + val.passiveListeners, 90 | 0 91 | ), 92 | onceListeners: table.reduce((acc, val) => acc + val.onceListeners, 0), 93 | references: "----", 94 | }, 95 | ...table, 96 | ]); 97 | } 98 | -------------------------------------------------------------------------------- /snippets/getNodesInfo/Readme.md: -------------------------------------------------------------------------------- 1 | ## Description 2 | 3 | Gets estimations for DOM nodes that are part of the layout of the body or specific DOM node. 4 | 5 | Will include: 6 | 7 | - Summary (number of DOM nodes, number of nodes that will be processed by the browser, number of DOM nodes that are not processed by the browser (hidden nodes)). 8 | - Summary for DOM nodes that can be excluded from layout by applying display: none in addition to other hiding mechanisms. 9 | 10 | > IMPORTANT: If you use this script as bookmarklet it will always get information for document.body. If used from console directly any DOM element can be passed. 11 | 12 | ## How to use it 13 | 14 | <!-- START-HOW_TO[bookmark,console-tab,sources-tab,chromium] --> 15 | 16 | 17 | | Technique | Is Usable | 18 | | ----------- | ---------- | 19 | | [Bookmark](https://github.com/push-based/web-performance-tools/blob/main/docs/how-to-use-it-with-bookmarks) | ✔ | 20 | | [Console Tab](https://github.com/push-based/web-performance-tools/blob/main/docs/how-to-use-it-with-console-tab.md) | ✔ | 21 | | [Sources Tab](https://github.com/push-based/web-performance-tools/blob/main/docs/how-to-use-it-with-sources-tab.md) | ✔ | 22 | | [Chromium](https://github.com/push-based/web-performance-tools/blob/main/docs/how-to-use-it-with-chromium.md) | ✔ | 23 | 24 | 25 | 26 | ### Bookmark Snippet 27 | 28 | 29 | 30 | <details> 31 | 32 | <summary>Copy this code snippet into the bookmark to use it</summary> 33 | 34 | 35 | ```javascript 36 | 37 | javascript:(() => {function index(root = document.body) { 38 | const allNodes = [...root.querySelectorAll("*")]; 39 | const notProcessed = allNodes.filter((n) => isHidden(n)); 40 | const processed = allNodes.filter((n) => !isHidden(n)); 41 | const visibility = processed.filter((n) => isVisibilityHidden(n)); 42 | const opacity = processed.filter((n) => isOpacity0(n)); 43 | const dimensions = processed.filter((n) => isHeightWidthOverflow(n)); 44 | const transform = processed.filter((n) => isTransformHidden(n)); 45 | const opacityFilter = processed.filter((n) => isFilterOpacity(n)); 46 | /** 47 | * Finds elements that are not affecting layout of the page and will not be included in styles recalculation 48 | */ 49 | function isHidden(element) { 50 | return !(element.offsetWidth || 51 | element.offsetHeight || 52 | element.getClientRects().length); 53 | } 54 | /** 55 | * This elements are still processed during style recalculation 56 | */ 57 | function isVisibilityHidden(element) { 58 | return window.getComputedStyle(element).visibility === "hidden"; 59 | } 60 | /** 61 | * This elements are still processed during style recalculation 62 | */ 63 | function isOpacity0(element) { 64 | return window.getComputedStyle(element).opacity === "0"; 65 | } 66 | /** 67 | * This elements are still processed during style recalculation 68 | */ 69 | function isHeightWidthOverflow(element) { 70 | const styles = window.getComputedStyle(element); 71 | return (((styles.height === "0" || styles.height === "0px") && 72 | styles.overflow === "hidden") || 73 | ((styles.width === "0" || styles.width === "0px") && 74 | styles.overflow === "hidden") || 75 | ((styles.height === "0" || 76 | (styles.height === "0px" && styles.width === "0") || 77 | styles.width === "0px") && 78 | styles.overflow === "hidden")); 79 | } 80 | /** 81 | * This elements are still processed during style recalculation 82 | */ 83 | function isTransformHidden(element) { 84 | return element.style.tranform === "scale(0)"; 85 | } 86 | /** 87 | * This elements are still processed during style recalculation 88 | */ 89 | function isFilterOpacity(element) { 90 | return element.style.filter === "opacity(0)"; 91 | } 92 | /** 93 | * This elements are still processed during style recalculation 94 | */ 95 | function getReferences(nodes) { 96 | return nodes.map((n) => ({ 97 | self: n, 98 | children: n.querySelectorAll("*"), 99 | })); 100 | } 101 | function getSummary(name, nodes) { 102 | const children = nodes 103 | .map((n) => n.querySelectorAll("*").length + 1) 104 | .reduce((acc, val) => acc + val, 0); 105 | return { 106 | "hiding method": name, 107 | nodes: nodes.length, 108 | children, 109 | "potential savings (%)": Number(parseFloat((children / processed.length) * 100).toFixed(2)), 110 | references: getReferences(nodes), 111 | }; 112 | } 113 | console.table([ 114 | { 115 | name: `📝TOTAL`, 116 | nodes: allNodes.length, 117 | processed: processed.length, 118 | notProcessed: notProcessed.length, 119 | }, 120 | ]); 121 | const summary = [ 122 | getSummary("visibility: none", visibility), 123 | getSummary("opacity: 0", opacity), 124 | getSummary("height: 0 || width: 0 && overflow: hidden", dimensions), 125 | getSummary("transform: scale(0)", transform), 126 | getSummary("filter: opacity(0)", opacityFilter), 127 | ]; 128 | return console.table([ 129 | { 130 | "hiding method": "👉SUMMARY", 131 | nodes: summary.reduce((acc, val) => acc + val.nodes, 0), 132 | children: summary.reduce((acc, val) => acc + val.children, 0), 133 | "potential savings (%)": Number(summary 134 | .reduce((acc, val) => acc + val["potential savings (%)"], 0) 135 | .toFixed(2)), 136 | references: "----", 137 | }, 138 | ...summary, 139 | ]); 140 | } 141 | })() 142 | ``` 143 | 144 | 145 | 146 | 147 | </details> 148 | 149 | 150 | 151 | ## Console Tab Snippet 152 | 153 | <details> 154 | 155 | <summary>Copy this code snippet into the DevTools console Tab to use it</summary> 156 | 157 | 158 | ```javascript 159 | 160 | function index(root = document.body) { 161 | const allNodes = [...root.querySelectorAll("*")]; 162 | const notProcessed = allNodes.filter((n) => isHidden(n)); 163 | const processed = allNodes.filter((n) => !isHidden(n)); 164 | const visibility = processed.filter((n) => isVisibilityHidden(n)); 165 | const opacity = processed.filter((n) => isOpacity0(n)); 166 | const dimensions = processed.filter((n) => isHeightWidthOverflow(n)); 167 | const transform = processed.filter((n) => isTransformHidden(n)); 168 | const opacityFilter = processed.filter((n) => isFilterOpacity(n)); 169 | /** 170 | * Finds elements that are not affecting layout of the page and will not be included in styles recalculation 171 | */ 172 | function isHidden(element) { 173 | return !(element.offsetWidth || 174 | element.offsetHeight || 175 | element.getClientRects().length); 176 | } 177 | /** 178 | * This elements are still processed during style recalculation 179 | */ 180 | function isVisibilityHidden(element) { 181 | return window.getComputedStyle(element).visibility === "hidden"; 182 | } 183 | /** 184 | * This elements are still processed during style recalculation 185 | */ 186 | function isOpacity0(element) { 187 | return window.getComputedStyle(element).opacity === "0"; 188 | } 189 | /** 190 | * This elements are still processed during style recalculation 191 | */ 192 | function isHeightWidthOverflow(element) { 193 | const styles = window.getComputedStyle(element); 194 | return (((styles.height === "0" || styles.height === "0px") && 195 | styles.overflow === "hidden") || 196 | ((styles.width === "0" || styles.width === "0px") && 197 | styles.overflow === "hidden") || 198 | ((styles.height === "0" || 199 | (styles.height === "0px" && styles.width === "0") || 200 | styles.width === "0px") && 201 | styles.overflow === "hidden")); 202 | } 203 | /** 204 | * This elements are still processed during style recalculation 205 | */ 206 | function isTransformHidden(element) { 207 | return element.style.tranform === "scale(0)"; 208 | } 209 | /** 210 | * This elements are still processed during style recalculation 211 | */ 212 | function isFilterOpacity(element) { 213 | return element.style.filter === "opacity(0)"; 214 | } 215 | /** 216 | * This elements are still processed during style recalculation 217 | */ 218 | function getReferences(nodes) { 219 | return nodes.map((n) => ({ 220 | self: n, 221 | children: n.querySelectorAll("*"), 222 | })); 223 | } 224 | function getSummary(name, nodes) { 225 | const children = nodes 226 | .map((n) => n.querySelectorAll("*").length + 1) 227 | .reduce((acc, val) => acc + val, 0); 228 | return { 229 | "hiding method": name, 230 | nodes: nodes.length, 231 | children, 232 | "potential savings (%)": Number(parseFloat((children / processed.length) * 100).toFixed(2)), 233 | references: getReferences(nodes), 234 | }; 235 | } 236 | console.table([ 237 | { 238 | name: `📝TOTAL`, 239 | nodes: allNodes.length, 240 | processed: processed.length, 241 | notProcessed: notProcessed.length, 242 | }, 243 | ]); 244 | const summary = [ 245 | getSummary("visibility: none", visibility), 246 | getSummary("opacity: 0", opacity), 247 | getSummary("height: 0 || width: 0 && overflow: hidden", dimensions), 248 | getSummary("transform: scale(0)", transform), 249 | getSummary("filter: opacity(0)", opacityFilter), 250 | ]; 251 | return console.table([ 252 | { 253 | "hiding method": "👉SUMMARY", 254 | nodes: summary.reduce((acc, val) => acc + val.nodes, 0), 255 | children: summary.reduce((acc, val) => acc + val.children, 0), 256 | "potential savings (%)": Number(summary 257 | .reduce((acc, val) => acc + val["potential savings (%)"], 0) 258 | .toFixed(2)), 259 | references: "----", 260 | }, 261 | ...summary, 262 | ]); 263 | } 264 | 265 | ``` 266 | 267 | 268 | 269 | 270 | </details> 271 | 272 | 273 | 274 | 275 | <!-- END-HOW_TO --> 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | 365 | 366 | 367 | 368 | 369 | 370 | 371 | 372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | ## Input 381 | 382 | - Root node for the calculations (default is document.body) 383 | 384 | ## Features 385 | 386 | - Provides information about DOM size of the body or specific DOM element. 387 | - Summary will include next information about DOM nodes: 388 | - Total number of DOM nodes 389 | - Number of DOM nodes that are part of the layouting process (will be included in recalculate styles work) 390 | - Number of DOM nodes that are excluded from the layouting process (display: none; for example) 391 | - Detailed information aboud DOM nodes that are hidden but still part of layouting process (visibility: hidden, opacity: 0 etc.) 392 | -------------------------------------------------------------------------------- /snippets/getNodesInfo/index.js: -------------------------------------------------------------------------------- 1 | function index(root = document.body) { 2 | const allNodes = [...root.querySelectorAll("*")]; 3 | const notProcessed = allNodes.filter((n) => isHidden(n)); 4 | const processed = allNodes.filter((n) => !isHidden(n)); 5 | const visibility = processed.filter((n) => isVisibilityHidden(n)); 6 | const opacity = processed.filter((n) => isOpacity0(n)); 7 | const dimensions = processed.filter((n) => isHeightWidthOverflow(n)); 8 | const transform = processed.filter((n) => isTransformHidden(n)); 9 | const opacityFilter = processed.filter((n) => isFilterOpacity(n)); 10 | 11 | /** 12 | * Finds elements that are not affecting layout of the page and will not be included in styles recalculation 13 | */ 14 | function isHidden(element) { 15 | return !( 16 | element.offsetWidth || 17 | element.offsetHeight || 18 | element.getClientRects().length 19 | ); 20 | } 21 | 22 | /** 23 | * This elements are still processed during style recalculation 24 | */ 25 | function isVisibilityHidden(element) { 26 | return window.getComputedStyle(element).visibility === "hidden"; 27 | } 28 | 29 | /** 30 | * This elements are still processed during style recalculation 31 | */ 32 | function isOpacity0(element) { 33 | return window.getComputedStyle(element).opacity === "0"; 34 | } 35 | 36 | /** 37 | * This elements are still processed during style recalculation 38 | */ 39 | function isHeightWidthOverflow(element) { 40 | const styles = window.getComputedStyle(element); 41 | 42 | return ( 43 | ((styles.height === "0" || styles.height === "0px") && 44 | styles.overflow === "hidden") || 45 | ((styles.width === "0" || styles.width === "0px") && 46 | styles.overflow === "hidden") || 47 | ((styles.height === "0" || 48 | (styles.height === "0px" && styles.width === "0") || 49 | styles.width === "0px") && 50 | styles.overflow === "hidden") 51 | ); 52 | } 53 | 54 | /** 55 | * This elements are still processed during style recalculation 56 | */ 57 | function isTransformHidden(element) { 58 | return element.style.tranform === "scale(0)"; 59 | } 60 | 61 | /** 62 | * This elements are still processed during style recalculation 63 | */ 64 | function isFilterOpacity(element) { 65 | return element.style.filter === "opacity(0)"; 66 | } 67 | 68 | /** 69 | * This elements are still processed during style recalculation 70 | */ 71 | function getReferences(nodes) { 72 | return nodes.map((n) => ({ 73 | self: n, 74 | children: n.querySelectorAll("*"), 75 | })); 76 | } 77 | 78 | function getSummary(name, nodes) { 79 | const children = nodes 80 | .map((n) => n.querySelectorAll("*").length + 1) 81 | .reduce((acc, val) => acc + val, 0); 82 | return { 83 | "hiding method": name, 84 | nodes: nodes.length, 85 | children, 86 | "potential savings (%)": Number( 87 | parseFloat((children / processed.length) * 100).toFixed(2) 88 | ), 89 | references: getReferences(nodes), 90 | }; 91 | } 92 | 93 | console.table([ 94 | { 95 | name: `📝TOTAL`, 96 | nodes: allNodes.length, 97 | processed: processed.length, 98 | notProcessed: notProcessed.length, 99 | }, 100 | ]); 101 | const summary = [ 102 | getSummary("visibility: none", visibility), 103 | getSummary("opacity: 0", opacity), 104 | getSummary("height: 0 || width: 0 && overflow: hidden", dimensions), 105 | getSummary("transform: scale(0)", transform), 106 | getSummary("filter: opacity(0)", opacityFilter), 107 | ]; 108 | return console.table([ 109 | { 110 | "hiding method": "👉SUMMARY", 111 | nodes: summary.reduce((acc, val) => acc + val.nodes, 0), 112 | children: summary.reduce((acc, val) => acc + val.children, 0), 113 | "potential savings (%)": Number( 114 | summary 115 | .reduce((acc, val) => acc + val["potential savings (%)"], 0) 116 | .toFixed(2) 117 | ), 118 | references: "----", 119 | }, 120 | ...summary, 121 | ]); 122 | } 123 | -------------------------------------------------------------------------------- /snippets/index.ts: -------------------------------------------------------------------------------- 1 | export * from './utils'; 2 | export * from './types'; 3 | export * from './constants'; 4 | -------------------------------------------------------------------------------- /snippets/lcp/Readme.md: -------------------------------------------------------------------------------- 1 | ## Description 2 | 3 | > Note: 4 | > This snippet is borrowed from [github.com/nucliweb/webperf-snippets](https://github.com/nucliweb/webperf-snippets/blob/main/README.md#largest-contentful-paint-lcp). 5 | 6 | Observes (also from the past) and logg's the [LCP](web.dev/lcp). 7 | 8 | ## How to use it 9 | 10 | <!-- START-HOW_TO[] --> 11 | 12 | 13 | 14 | 15 | ### Bookmark Snippet 16 | 17 | 18 | 19 | <details> 20 | 21 | <summary>Copy this code snippet into the bookmark to use it</summary> 22 | 23 | 24 | ```javascript 25 | 26 | javascript:(() => {/** 27 | * PerformanceObserver 28 | */ 29 | const po = new PerformanceObserver((list) => { 30 | let entries = list.getEntries(); 31 | entries = dedupe(entries, "startTime"); 32 | /** 33 | * Print all entries of LCP 34 | */ 35 | entries.forEach((item, i) => { 36 | console.dir(item); 37 | console.log(`${i + 1} current LCP item : ${item.element}: ${item.startTime}`); 38 | /** 39 | * Highlight LCP elements on the page 40 | */ 41 | item.element ? (item.element.style = "border: 5px dotted blue;") : console.warn('LCP not highlighted'); 42 | }); 43 | /** 44 | * LCP is the lastEntry in getEntries Array 45 | */ 46 | const lastEntry = entries[entries.length - 1]; 47 | /** 48 | * Print final LCP 49 | */ 50 | console.log(`LCP is: ${lastEntry.startTime}`); 51 | }); 52 | /** 53 | * Start observing for largest-contentful-paint 54 | * buffered true getEntries prior to this script execution 55 | */ 56 | po.observe({ type: "largest-contentful-paint", buffered: true }); 57 | function dedupe(arr, key) { 58 | return [...new Map(arr.map((item) => [item[key], item])).values()]; 59 | } 60 | })() 61 | ``` 62 | 63 | 64 | 65 | 66 | </details> 67 | 68 | 69 | 70 | ## Console Tab Snippet 71 | 72 | <details> 73 | 74 | <summary>Copy this code snippet into the DevTools console Tab to use it</summary> 75 | 76 | 77 | ```javascript 78 | 79 | /** 80 | * PerformanceObserver 81 | */ 82 | const po = new PerformanceObserver((list) => { 83 | let entries = list.getEntries(); 84 | entries = dedupe(entries, "startTime"); 85 | /** 86 | * Print all entries of LCP 87 | */ 88 | entries.forEach((item, i) => { 89 | console.dir(item); 90 | console.log(`${i + 1} current LCP item : ${item.element}: ${item.startTime}`); 91 | /** 92 | * Highlight LCP elements on the page 93 | */ 94 | item.element ? (item.element.style = "border: 5px dotted blue;") : console.warn('LCP not highlighted'); 95 | }); 96 | /** 97 | * LCP is the lastEntry in getEntries Array 98 | */ 99 | const lastEntry = entries[entries.length - 1]; 100 | /** 101 | * Print final LCP 102 | */ 103 | console.log(`LCP is: ${lastEntry.startTime}`); 104 | }); 105 | /** 106 | * Start observing for largest-contentful-paint 107 | * buffered true getEntries prior to this script execution 108 | */ 109 | po.observe({ type: "largest-contentful-paint", buffered: true }); 110 | function dedupe(arr, key) { 111 | return [...new Map(arr.map((item) => [item[key], item])).values()]; 112 | } 113 | 114 | ``` 115 | 116 | 117 | 118 | 119 | </details> 120 | 121 | 122 | 123 | 124 | <!-- END-HOW_TO --> 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | -------------------------------------------------------------------------------- /snippets/lcp/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * PerformanceObserver 3 | */ 4 | const po = new PerformanceObserver((list) => { 5 | let entries = list.getEntries(); 6 | 7 | entries = dedupe(entries, "startTime"); 8 | 9 | /** 10 | * Print all entries of LCP 11 | */ 12 | entries.forEach((item, i) => { 13 | console.dir(item); 14 | console.log( 15 | `${i + 1} current LCP item : ${item.element}: ${item.startTime}` 16 | ); 17 | /** 18 | * Highlight LCP elements on the page 19 | */ 20 | item.element ? (item.element.style = "border: 5px dotted blue;") : console.warn('LCP not highlighted'); 21 | }); 22 | 23 | /** 24 | * LCP is the lastEntry in getEntries Array 25 | */ 26 | const lastEntry = entries[entries.length - 1]; 27 | /** 28 | * Print final LCP 29 | */ 30 | console.log(`LCP is: ${lastEntry.startTime}`); 31 | }); 32 | 33 | /** 34 | * Start observing for largest-contentful-paint 35 | * buffered true getEntries prior to this script execution 36 | */ 37 | po.observe({ type: "largest-contentful-paint", buffered: true }); 38 | 39 | function dedupe(arr, key) { 40 | return [...new Map(arr.map((item) => [item[key], item])).values()]; 41 | } 42 | -------------------------------------------------------------------------------- /snippets/long-task/Readme.md: -------------------------------------------------------------------------------- 1 | ## Description 2 | 3 | > Note: 4 | > This snippet is borrowed from [github.com/nucliweb/webperf-snippets](https://github.com/nucliweb/webperf-snippets/blob/main/README.md#long-task). 5 | 6 | To determine when long tasks happen, you can use [PerformanceObserver](https://developer.mozilla.org/docs/Web/API/PerformanceObserver) and register to observe entries of type `longtask`: 7 | 8 | ## How to use it 9 | 10 | <!-- START-HOW_TO[bookmark,console-tab,sources-tab,chromium] --> 11 | 12 | 13 | | Technique | Is Usable | 14 | | ----------- | ---------- | 15 | | [Bookmark](https://github.com/push-based/web-performance-tools/blob/main/docs/how-to-use-it-with-bookmarks) | ✔ | 16 | | [Console Tab](https://github.com/push-based/web-performance-tools/blob/main/docs/how-to-use-it-with-console-tab.md) | ✔ | 17 | | [Sources Tab](https://github.com/push-based/web-performance-tools/blob/main/docs/how-to-use-it-with-sources-tab.md) | ✔ | 18 | | [Chromium](https://github.com/push-based/web-performance-tools/blob/main/docs/how-to-use-it-with-chromium.md) | ✔ | 19 | 20 | 21 | 22 | ### Bookmark Snippet 23 | 24 | 25 | 26 | <details> 27 | 28 | <summary>Copy this code snippet into the bookmark to use it</summary> 29 | 30 | 31 | ```javascript 32 | 33 | javascript:(() => {try { 34 | // Create the performance observer. 35 | const po = new PerformanceObserver((list) => { 36 | for (const entry of list.getEntries()) { 37 | // Log the entry and all associated details. 38 | console.table(entry.toJSON()); 39 | } 40 | }); 41 | // Start listening for `longtask` entries to be dispatched. 42 | po.observe({ type: 'longtask', buffered: true }); 43 | } 44 | catch (e) { 45 | console.log(`The browser doesn't support this API`); 46 | } 47 | })() 48 | ``` 49 | 50 | 51 | 52 | 53 | </details> 54 | 55 | 56 | 57 | ## Console Tab Snippet 58 | 59 | <details> 60 | 61 | <summary>Copy this code snippet into the DevTools console Tab to use it</summary> 62 | 63 | 64 | ```javascript 65 | 66 | try { 67 | // Create the performance observer. 68 | const po = new PerformanceObserver((list) => { 69 | for (const entry of list.getEntries()) { 70 | // Log the entry and all associated details. 71 | console.table(entry.toJSON()); 72 | } 73 | }); 74 | // Start listening for `longtask` entries to be dispatched. 75 | po.observe({ type: 'longtask', buffered: true }); 76 | } 77 | catch (e) { 78 | console.log(`The browser doesn't support this API`); 79 | } 80 | 81 | ``` 82 | 83 | 84 | 85 | 86 | </details> 87 | 88 | 89 | 90 | 91 | <!-- END-HOW_TO --> 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | -------------------------------------------------------------------------------- /snippets/long-task/index.js: -------------------------------------------------------------------------------- 1 | try { 2 | // Create the performance observer. 3 | const po = new PerformanceObserver((list) => { 4 | for (const entry of list.getEntries()) { 5 | // Log the entry and all associated details. 6 | console.table(entry.toJSON()); 7 | } 8 | }); 9 | // Start listening for `longtask` entries to be dispatched. 10 | po.observe({type: 'longtask', buffered: true}); 11 | } catch (e) { 12 | console.log(`The browser doesn't support this API`) 13 | } 14 | -------------------------------------------------------------------------------- /snippets/make-lazy-img/Readme.md: -------------------------------------------------------------------------------- 1 | ## Description 2 | 3 | Add `loading="lazy"` to all images and iframe's on the page. 4 | 5 | ## How to use it 6 | 7 | <!-- START-HOW_TO[] --> 8 | 9 | 10 | 11 | 12 | ### Bookmark Snippet 13 | 14 | 15 | 16 | <details> 17 | 18 | <summary>Copy this code snippet into the bookmark to use it</summary> 19 | 20 | 21 | ```javascript 22 | 23 | javascript:(() => {const imgs = document.querySelectorAll('img'); 24 | Array.from(imgs) 25 | .forEach(i => i.setAttribute('loading', 'lazy')); 26 | })() 27 | ``` 28 | 29 | 30 | 31 | 32 | </details> 33 | 34 | 35 | 36 | ## Console Tab Snippet 37 | 38 | <details> 39 | 40 | <summary>Copy this code snippet into the DevTools console Tab to use it</summary> 41 | 42 | 43 | ```javascript 44 | 45 | const imgs = document.querySelectorAll('img'); 46 | Array.from(imgs) 47 | .forEach(i => i.setAttribute('loading', 'lazy')); 48 | 49 | ``` 50 | 51 | 52 | 53 | 54 | </details> 55 | 56 | 57 | 58 | 59 | <!-- END-HOW_TO --> 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 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | -------------------------------------------------------------------------------- /snippets/make-lazy-img/images/check-lazy-imgs.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/push-based/awesome-web-performance-snippets/2273b69d62e12f44520a5d61cf8cd6c1fa6131b6/snippets/make-lazy-img/images/check-lazy-imgs.PNG -------------------------------------------------------------------------------- /snippets/make-lazy-img/index.js: -------------------------------------------------------------------------------- 1 | const imgs = document.querySelectorAll('img'); 2 | Array.from(imgs) 3 | .forEach(i => i.setAttribute('loading', 'lazy')); 4 | -------------------------------------------------------------------------------- /snippets/optimize-svg-usage/Readme.md: -------------------------------------------------------------------------------- 1 | # Optimize SVG usage 2 | 3 | ## Description 4 | 5 | This scripts mimics the usage of [ngx-fast-svg](https://github.com/push-based/ngx-fast-svg) and it's impact. 6 | 7 | ## How to use it 8 | 9 | 1. Count DOM nodes of all SVG elements 10 | ```javascript 11 | console.log('Number of SVG elements inc content', document.querySelectorAll('svg, svg *').length) 12 | ``` 13 | 1.1. Additionally measure Layouting cost [full-relayout](https://github.com/push-based/awesome-web-performance-snippets/tree/main/snippets/full-relayout). 14 | 2. run script 15 | 3. Remeasure 1. and 1.1. 16 | 17 | <!-- START-HOW_TO[bookmark,console-tab,sources-tab,chromium] --> 18 | 19 | 20 | | Technique | Is Usable | 21 | | ----------- | ---------- | 22 | | [Bookmark](https://github.com/push-based/web-performance-tools/blob/main/docs/how-to-use-it-with-bookmarks) | ✔ | 23 | | [Console Tab](https://github.com/push-based/web-performance-tools/blob/main/docs/how-to-use-it-with-console-tab.md) | ✔ | 24 | | [Sources Tab](https://github.com/push-based/web-performance-tools/blob/main/docs/how-to-use-it-with-sources-tab.md) | ✔ | 25 | | [Chromium](https://github.com/push-based/web-performance-tools/blob/main/docs/how-to-use-it-with-chromium.md) | ✔ | 26 | 27 | 28 | 29 | ### Bookmark Snippet 30 | 31 | 32 | 33 | <details> 34 | 35 | <summary>Copy this code snippet into the bookmark to use it</summary> 36 | 37 | 38 | ```javascript 39 | 40 | javascript:(() => {function createElementFromHTMLString(htmlString) { 41 | var div = document.createElement('div'); 42 | div.innerHTML = htmlString.trim(); 43 | return div.firstChild; 44 | } 45 | function cacheInDom(svgElem, svgId) { 46 | const node = svgElem.cloneNode(svgElem); 47 | node?.setAttribute && node.setAttribute('id', svgId); 48 | svgDomCache.appendChild(node); 49 | } 50 | function modifySvgToUseCache(svgElem, svgId) { 51 | //svgElem.replaceWith(createElementFromHTMLString(`<svg><use href="#${svgId}"></use></svg>`)); 52 | svgElem.innerHTML = `<use href="#${svgId}"></use>`; 53 | } 54 | let nextCachedSvgId = Math.random(); 55 | const svgDomCacheHtml = `<div id="svg-cache" style=" 56 | overflow: hidden; 57 | width: 0; 58 | height: 0; 59 | position: fixed; 60 | bottom: -2000px; 61 | contain: content; 62 | content-visibility: auto; 63 | "></div>`; 64 | const svgDomCache = createElementFromHTMLString(svgDomCacheHtml); 65 | document.body.appendChild(svgDomCache); 66 | let reusedDomNodes = 0; 67 | const cachedSvgContent = new Set(); 68 | document.querySelectorAll('svg').forEach(svg => { 69 | if (svg.children[0].tagName !== 'use') { 70 | if (!cachedSvgContent.has(svg.innerHTML)) { 71 | nextCachedSvgId++; 72 | cachedSvgContent.add(svg.innerHTML); 73 | cacheInDom(svg, nextCachedSvgId); 74 | } 75 | else { 76 | reusedDomNodes += svg.querySelectorAll('*').length; 77 | } 78 | modifySvgToUseCache(svg, nextCachedSvgId); 79 | } 80 | else { 81 | console.info('already optimized'); 82 | } 83 | }); 84 | console.log('Reused DOM nodes: ', reusedDomNodes); 85 | })() 86 | ``` 87 | 88 | 89 | 90 | 91 | </details> 92 | 93 | 94 | 95 | ## Console Tab Snippet 96 | 97 | <details> 98 | 99 | <summary>Copy this code snippet into the DevTools console Tab to use it</summary> 100 | 101 | 102 | ```javascript 103 | 104 | function createElementFromHTMLString(htmlString) { 105 | var div = document.createElement('div'); 106 | div.innerHTML = htmlString.trim(); 107 | return div.firstChild; 108 | } 109 | function cacheInDom(svgElem, svgId) { 110 | const node = svgElem.cloneNode(svgElem); 111 | node?.setAttribute && node.setAttribute('id', svgId); 112 | svgDomCache.appendChild(node); 113 | } 114 | function modifySvgToUseCache(svgElem, svgId) { 115 | //svgElem.replaceWith(createElementFromHTMLString(`<svg><use href="#${svgId}"></use></svg>`)); 116 | svgElem.innerHTML = `<use href="#${svgId}"></use>`; 117 | } 118 | let nextCachedSvgId = Math.random(); 119 | const svgDomCacheHtml = `<div id="svg-cache" style=" 120 | overflow: hidden; 121 | width: 0; 122 | height: 0; 123 | position: fixed; 124 | bottom: -2000px; 125 | contain: content; 126 | content-visibility: auto; 127 | "></div>`; 128 | const svgDomCache = createElementFromHTMLString(svgDomCacheHtml); 129 | document.body.appendChild(svgDomCache); 130 | let reusedDomNodes = 0; 131 | const cachedSvgContent = new Set(); 132 | document.querySelectorAll('svg').forEach(svg => { 133 | if (svg.children[0].tagName !== 'use') { 134 | if (!cachedSvgContent.has(svg.innerHTML)) { 135 | nextCachedSvgId++; 136 | cachedSvgContent.add(svg.innerHTML); 137 | cacheInDom(svg, nextCachedSvgId); 138 | } 139 | else { 140 | reusedDomNodes += svg.querySelectorAll('*').length; 141 | } 142 | modifySvgToUseCache(svg, nextCachedSvgId); 143 | } 144 | else { 145 | console.info('already optimized'); 146 | } 147 | }); 148 | console.log('Reused DOM nodes: ', reusedDomNodes); 149 | 150 | ``` 151 | 152 | 153 | 154 | 155 | </details> 156 | 157 | 158 | 159 | 160 | <!-- END-HOW_TO --> 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | -------------------------------------------------------------------------------- /snippets/optimize-svg-usage/index.ts: -------------------------------------------------------------------------------- 1 | function createElementFromHTMLString(htmlString) { 2 | var div = document.createElement('div'); 3 | div.innerHTML = htmlString.trim(); 4 | return div.firstChild; 5 | } 6 | 7 | function cacheInDom(svgElem, svgId) { 8 | const node = svgElem.cloneNode(svgElem); 9 | node?.setAttribute && node.setAttribute('id', svgId); 10 | svgDomCache.appendChild(node); 11 | } 12 | 13 | function modifySvgToUseCache(svgElem, svgId) { 14 | //svgElem.replaceWith(createElementFromHTMLString(`<svg><use href="#${svgId}"></use></svg>`)); 15 | svgElem.innerHTML = `<use href="#${svgId}"></use>`; 16 | } 17 | 18 | let nextCachedSvgId = Math.random(); 19 | const svgDomCacheHtml = `<div id="svg-cache" style=" 20 | overflow: hidden; 21 | width: 0; 22 | height: 0; 23 | position: fixed; 24 | bottom: -2000px; 25 | contain: content; 26 | content-visibility: auto; 27 | "></div>`; 28 | const svgDomCache = createElementFromHTMLString(svgDomCacheHtml); 29 | document.body.appendChild(svgDomCache); 30 | 31 | let reusedDomNodes = 0; 32 | const cachedSvgContent = new Set(); 33 | document.querySelectorAll('svg').forEach(svg => { 34 | if (svg.children[0].tagName !== 'use') { 35 | if (!cachedSvgContent.has(svg.innerHTML)) { 36 | nextCachedSvgId++; 37 | cachedSvgContent.add(svg.innerHTML); 38 | cacheInDom(svg, nextCachedSvgId); 39 | } else { 40 | reusedDomNodes += svg.querySelectorAll('*').length; 41 | } 42 | modifySvgToUseCache(svg, nextCachedSvgId); 43 | } else { 44 | console.info('already optimized'); 45 | } 46 | }); 47 | console.log('Reused DOM nodes: ', reusedDomNodes); 48 | -------------------------------------------------------------------------------- /snippets/re-apply-dom/Readme.md: -------------------------------------------------------------------------------- 1 | # Re apply DOM nodes 2 | 3 | ## Description 4 | 5 | This snippet removed all dom nodes from the body and adds it again after 350ms. 6 | 7 | It can be used to test lazy loading of images and other improvements on the dom structure without reloading the page. 8 | ## How to use it 9 | 10 | 1. start recording 11 | 2. execute script one or multiple times 12 | 3. stop recording 13 | 14 | Use case: add correct lazy loading to images 15 | 16 | 1. load the page and wait until network is saddeled 17 | 2. execute the script and count the loaded images 18 | <img width="216" alt="loading-lazy_before" src="https://user-images.githubusercontent.com/10064416/206700058-8270f18b-5316-45a0-8e1f-1a5b203bdee3.PNG"> 19 | 3. modify the HTML and add lazy loading wherever it fit's. Here we used the "check images usage" snippet to do it 20 | <img width="595" alt="loading1" src="https://user-images.githubusercontent.com/10064416/206700054-a322b91d-5977-43f9-be50-ea55c3286a42.PNG"> 21 | 4. execute the script and count the loaded images 22 | <img width="283" alt="loading-lazy_after" src="https://user-images.githubusercontent.com/10064416/206700055-4b6f34b9-6735-4907-901a-c31f59246ae6.PNG"> 23 | <!-- START-HOW_TO[bookmark,console-tab,sources-tab,chromium] --> 24 | 25 | 26 | | Technique | Is Usable | 27 | | ----------- | ---------- | 28 | | [Bookmark](https://github.com/push-based/web-performance-tools/blob/main/docs/how-to-use-it-with-bookmarks) | ✔ | 29 | | [Console Tab](https://github.com/push-based/web-performance-tools/blob/main/docs/how-to-use-it-with-console-tab.md) | ✔ | 30 | | [Sources Tab](https://github.com/push-based/web-performance-tools/blob/main/docs/how-to-use-it-with-sources-tab.md) | ✔ | 31 | | [Chromium](https://github.com/push-based/web-performance-tools/blob/main/docs/how-to-use-it-with-chromium.md) | ✔ | 32 | 33 | 34 | 35 | ### Bookmark Snippet 36 | 37 | 38 | 39 | <details> 40 | 41 | <summary>Copy this code snippet into the bookmark to use it</summary> 42 | 43 | 44 | ```javascript 45 | 46 | javascript:(() => {const bi = document.body.innerHTML; 47 | document.body.innerHTML = ''; 48 | setTimeout(() => document.body.innerHTML = bi, 350); 49 | })() 50 | ``` 51 | 52 | 53 | 54 | 55 | </details> 56 | 57 | 58 | 59 | ## Console Tab Snippet 60 | 61 | <details> 62 | 63 | <summary>Copy this code snippet into the DevTools console Tab to use it</summary> 64 | 65 | 66 | ```javascript 67 | 68 | const bi = document.body.innerHTML; 69 | document.body.innerHTML = ''; 70 | setTimeout(() => document.body.innerHTML = bi, 350); 71 | 72 | ``` 73 | 74 | 75 | 76 | 77 | </details> 78 | 79 | 80 | 81 | 82 | <!-- END-HOW_TO --> 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 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | # Credits 188 | 189 | Author: _Michael Hladky - push-based.io_ 190 | Source: _[github.com/push-based/web-performance-tools](www.github.com/push-based/web-performance-tools)_ 191 | -------------------------------------------------------------------------------- /snippets/re-apply-dom/index.js: -------------------------------------------------------------------------------- 1 | const bi = document.body.innerHTML; 2 | document.body.innerHTML = ''; 3 | setTimeout(() => document.body.innerHTML = bi, 350); 4 | -------------------------------------------------------------------------------- /snippets/resources-hints/Readme.md: -------------------------------------------------------------------------------- 1 | ## Description 2 | 3 | > Note: 4 | > This snippet is borrowed from [github.com/nucliweb/webperf-snippets](https://github.com/nucliweb/webperf-snippets/blob/main/README.md#resources-hints). 5 | 6 | Check if the page has resources hints 7 | 8 | ## How to use it 9 | 10 | <!-- START-HOW_TO[] --> 11 | 12 | 13 | 14 | 15 | ### Bookmark Snippet 16 | 17 | 18 | 19 | <details> 20 | 21 | <summary>Copy this code snippet into the bookmark to use it</summary> 22 | 23 | 24 | ```javascript 25 | 26 | javascript:(() => {const rels = [ 27 | "preload", 28 | "prefetch", 29 | "preconnect", 30 | "dns-prefetch", 31 | "preconnect dns-prefetch", 32 | "prerender", 33 | "modulepreload", 34 | ]; 35 | rels.forEach((element) => { 36 | const linkElements = document.querySelectorAll(`link[rel="${element}"]`); 37 | const dot = linkElements.length > 0 ? "🟩" : "🟥"; 38 | console.log(`${dot} ${element}`); 39 | linkElements.forEach((el) => console.log(el)); 40 | }); 41 | })() 42 | ``` 43 | 44 | 45 | 46 | 47 | </details> 48 | 49 | 50 | 51 | ## Console Tab Snippet 52 | 53 | <details> 54 | 55 | <summary>Copy this code snippet into the DevTools console Tab to use it</summary> 56 | 57 | 58 | ```javascript 59 | 60 | const rels = [ 61 | "preload", 62 | "prefetch", 63 | "preconnect", 64 | "dns-prefetch", 65 | "preconnect dns-prefetch", 66 | "prerender", 67 | "modulepreload", 68 | ]; 69 | rels.forEach((element) => { 70 | const linkElements = document.querySelectorAll(`link[rel="${element}"]`); 71 | const dot = linkElements.length > 0 ? "🟩" : "🟥"; 72 | console.log(`${dot} ${element}`); 73 | linkElements.forEach((el) => console.log(el)); 74 | }); 75 | 76 | ``` 77 | 78 | 79 | 80 | 81 | </details> 82 | 83 | 84 | 85 | 86 | <!-- END-HOW_TO --> 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | -------------------------------------------------------------------------------- /snippets/resources-hints/index.js: -------------------------------------------------------------------------------- 1 | const rels = [ 2 | "preload", 3 | "prefetch", 4 | "preconnect", 5 | "dns-prefetch", 6 | "preconnect dns-prefetch", 7 | "prerender", 8 | "modulepreload", 9 | ]; 10 | 11 | rels.forEach((element) => { 12 | const linkElements = document.querySelectorAll(`link[rel="${element}"]`); 13 | const dot = linkElements.length > 0 ? "🟩" : "🟥"; 14 | console.log(`${dot} ${element}`); 15 | linkElements.forEach((el) => console.log(el)); 16 | }); 17 | -------------------------------------------------------------------------------- /snippets/scripts-loading/Readme.md: -------------------------------------------------------------------------------- 1 | ## Description 2 | 3 | > Note: 4 | > This snippet is borrowed from [github.com/nucliweb/webperf-snippets](https://github.com/nucliweb/webperf-snippets/blob/main/README.md#scripts-loading). 5 | 6 | List all the <scripts> in the DOM and show a table to see if are loaded async and/or defer 7 | 8 | ## How to use it 9 | 10 | <!-- START-HOW_TO[bookmark,console-tab,sources-tab,chromium] --> 11 | 12 | 13 | | Technique | Is Usable | 14 | | ----------- | ---------- | 15 | | [Bookmark](https://github.com/push-based/web-performance-tools/blob/main/docs/how-to-use-it-with-bookmarks) | ✔ | 16 | | [Console Tab](https://github.com/push-based/web-performance-tools/blob/main/docs/how-to-use-it-with-console-tab.md) | ✔ | 17 | | [Sources Tab](https://github.com/push-based/web-performance-tools/blob/main/docs/how-to-use-it-with-sources-tab.md) | ✔ | 18 | | [Chromium](https://github.com/push-based/web-performance-tools/blob/main/docs/how-to-use-it-with-chromium.md) | ✔ | 19 | 20 | 21 | 22 | ### Bookmark Snippet 23 | 24 | 25 | 26 | <details> 27 | 28 | <summary>Copy this code snippet into the bookmark to use it</summary> 29 | 30 | 31 | ```javascript 32 | 33 | javascript:(() => {const scripts = document.querySelectorAll('script[src]'); 34 | const scriptsLoading = [...scripts].map((obj) => { 35 | let newObj = {}; 36 | newObj = { 37 | src: obj.src, 38 | async: obj.async, 39 | defer: obj.defer, 40 | 'render blocking': obj.async || obj.defer ? '' : '🟥' 41 | }; 42 | return newObj; 43 | }); 44 | console.table(scriptsLoading); 45 | })() 46 | ``` 47 | 48 | 49 | 50 | 51 | </details> 52 | 53 | 54 | 55 | ## Console Tab Snippet 56 | 57 | <details> 58 | 59 | <summary>Copy this code snippet into the DevTools console Tab to use it</summary> 60 | 61 | 62 | ```javascript 63 | 64 | const scripts = document.querySelectorAll('script[src]'); 65 | const scriptsLoading = [...scripts].map((obj) => { 66 | let newObj = {}; 67 | newObj = { 68 | src: obj.src, 69 | async: obj.async, 70 | defer: obj.defer, 71 | 'render blocking': obj.async || obj.defer ? '' : '🟥' 72 | }; 73 | return newObj; 74 | }); 75 | console.table(scriptsLoading); 76 | 77 | ``` 78 | 79 | 80 | 81 | 82 | </details> 83 | 84 | 85 | 86 | 87 | <!-- END-HOW_TO --> 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | -------------------------------------------------------------------------------- /snippets/scripts-loading/index.js: -------------------------------------------------------------------------------- 1 | const scripts = document.querySelectorAll('script[src]'); 2 | 3 | const scriptsLoading = [...scripts].map((obj) => { 4 | let newObj = {}; 5 | newObj = { 6 | src: obj.src, 7 | async: obj.async, 8 | defer: obj.defer, 9 | 'render blocking': obj.async || obj.defer ? '' : '🟥' 10 | }; 11 | return newObj; 12 | }); 13 | console.table(scriptsLoading); 14 | -------------------------------------------------------------------------------- /snippets/scroll-up-down/Readme.md: -------------------------------------------------------------------------------- 1 | ## Description 2 | 3 | Scrolls down waits and scrolls up again. 4 | This is helpful to get scroll performance and interacted state of the page. 5 | 6 | ## How to use it 7 | 8 | <!-- START-HOW_TO[] --> 9 | 10 | 11 | 12 | 13 | ### Bookmark Snippet 14 | 15 | 16 | 17 | <details> 18 | 19 | <summary>Copy this code snippet into the bookmark to use it</summary> 20 | 21 | 22 | ```javascript 23 | 24 | javascript:(() => {const scrollHeight = document.documentElement.scrollHeight; 25 | window.scroll({ 26 | top: scrollHeight, 27 | behavior: 'smooth' 28 | }); 29 | // wait for a second, then scroll back up 30 | setTimeout(() => window.scroll({ 31 | top: 0, 32 | behavior: 'smooth' 33 | }), 3000); 34 | console.log('scroll done!'); 35 | })() 36 | ``` 37 | 38 | 39 | 40 | 41 | </details> 42 | 43 | 44 | 45 | ## Console Tab Snippet 46 | 47 | <details> 48 | 49 | <summary>Copy this code snippet into the DevTools console Tab to use it</summary> 50 | 51 | 52 | ```javascript 53 | 54 | const scrollHeight = document.documentElement.scrollHeight; 55 | window.scroll({ 56 | top: scrollHeight, 57 | behavior: 'smooth' 58 | }); 59 | // wait for a second, then scroll back up 60 | setTimeout(() => window.scroll({ 61 | top: 0, 62 | behavior: 'smooth' 63 | }), 3000); 64 | console.log('scroll done!'); 65 | 66 | ``` 67 | 68 | 69 | 70 | 71 | </details> 72 | 73 | 74 | 75 | 76 | <!-- END-HOW_TO --> 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 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | -------------------------------------------------------------------------------- /snippets/scroll-up-down/index.js: -------------------------------------------------------------------------------- 1 | const scrollHeight = document.documentElement.scrollHeight; 2 | 3 | window.scroll({ 4 | top: scrollHeight, 5 | behavior: 'smooth' 6 | }); 7 | 8 | // wait for a second, then scroll back up 9 | setTimeout(() => window.scroll({ 10 | top: 0, 11 | behavior: 'smooth' 12 | }), 3000); 13 | console.log('scroll done!'); 14 | -------------------------------------------------------------------------------- /snippets/time-to-first-byte-all/Readme.md: -------------------------------------------------------------------------------- 1 | ## Description 2 | 3 | > Note: 4 | > This snippet is borrowed from [github.com/nucliweb/webperf-snippets](https://github.com/nucliweb/webperf-snippets/blob/main/README.md#time-to-first-byte). 5 | 6 | Measure the time to first byte of all resources loaded 7 | 8 | ## How to use it 9 | 10 | <!-- START-HOW_TO[bookmark,console-tab,sources-tab,chromium] --> 11 | 12 | 13 | | Technique | Is Usable | 14 | | ----------- | ---------- | 15 | | [Bookmark](https://github.com/push-based/web-performance-tools/blob/main/docs/how-to-use-it-with-bookmarks) | ✔ | 16 | | [Console Tab](https://github.com/push-based/web-performance-tools/blob/main/docs/how-to-use-it-with-console-tab.md) | ✔ | 17 | | [Sources Tab](https://github.com/push-based/web-performance-tools/blob/main/docs/how-to-use-it-with-sources-tab.md) | ✔ | 18 | | [Chromium](https://github.com/push-based/web-performance-tools/blob/main/docs/how-to-use-it-with-chromium.md) | ✔ | 19 | 20 | 21 | 22 | ### Bookmark Snippet 23 | 24 | 25 | 26 | <details> 27 | 28 | <summary>Copy this code snippet into the bookmark to use it</summary> 29 | 30 | 31 | ```javascript 32 | 33 | javascript:(() => {new PerformanceObserver((entryList) => { 34 | const entries = entryList.getEntries(); 35 | const resourcesLoaded = [...entries].map((entry) => { 36 | let obj = {}; 37 | // Some resources may have a responseStart value of 0, due 38 | // to the resource being cached, or a cross-origin resource 39 | // being served without a Timing-Allow-Origin header set. 40 | if (entry.responseStart > 0) { 41 | obj = { 42 | 'TTFB (ms)': entry.responseStart, 43 | Resource: entry.name 44 | }; 45 | } 46 | return obj; 47 | }); 48 | console.table(resourcesLoaded); 49 | }).observe({ 50 | type: 'resource', 51 | buffered: true 52 | }); 53 | })() 54 | ``` 55 | 56 | 57 | 58 | 59 | </details> 60 | 61 | 62 | 63 | ## Console Tab Snippet 64 | 65 | <details> 66 | 67 | <summary>Copy this code snippet into the DevTools console Tab to use it</summary> 68 | 69 | 70 | ```javascript 71 | 72 | new PerformanceObserver((entryList) => { 73 | const entries = entryList.getEntries(); 74 | const resourcesLoaded = [...entries].map((entry) => { 75 | let obj = {}; 76 | // Some resources may have a responseStart value of 0, due 77 | // to the resource being cached, or a cross-origin resource 78 | // being served without a Timing-Allow-Origin header set. 79 | if (entry.responseStart > 0) { 80 | obj = { 81 | 'TTFB (ms)': entry.responseStart, 82 | Resource: entry.name 83 | }; 84 | } 85 | return obj; 86 | }); 87 | console.table(resourcesLoaded); 88 | }).observe({ 89 | type: 'resource', 90 | buffered: true 91 | }); 92 | 93 | ``` 94 | 95 | 96 | 97 | 98 | </details> 99 | 100 | 101 | 102 | 103 | <!-- END-HOW_TO --> 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | -------------------------------------------------------------------------------- /snippets/time-to-first-byte-all/index.js: -------------------------------------------------------------------------------- 1 | new PerformanceObserver((entryList) => { 2 | const entries = entryList.getEntries(); 3 | const resourcesLoaded = [...entries].map((entry) => { 4 | let obj= {}; 5 | // Some resources may have a responseStart value of 0, due 6 | // to the resource being cached, or a cross-origin resource 7 | // being served without a Timing-Allow-Origin header set. 8 | if (entry.responseStart > 0) { 9 | obj = { 10 | 'TTFB (ms)': entry.responseStart, 11 | Resource: entry.name 12 | } 13 | } 14 | return obj 15 | }) 16 | console.table(resourcesLoaded) 17 | }).observe({ 18 | type: 'resource', 19 | buffered: true 20 | }) 21 | -------------------------------------------------------------------------------- /snippets/time-to-first-byte-document/Readme.md: -------------------------------------------------------------------------------- 1 | ## Description 2 | 3 | > Note: 4 | > This snippet is borrowed from [github.com/nucliweb/webperf-snippets](https://github.com/nucliweb/webperf-snippets/blob/main/README.md#time-to-first-byte). 5 | 6 | Measure the time to first byte, from the document 7 | 8 | ## How to use it 9 | 10 | <!-- START-HOW_TO[] --> 11 | 12 | 13 | 14 | 15 | ### Bookmark Snippet 16 | 17 | 18 | 19 | <details> 20 | 21 | <summary>Copy this code snippet into the bookmark to use it</summary> 22 | 23 | 24 | ```javascript 25 | 26 | javascript:(() => {new PerformanceObserver((entryList) => { 27 | const [pageNav] = entryList.getEntriesByType('navigation'); 28 | console.log(`TTFB (ms): ${pageNav.responseStart}`); 29 | }).observe({ 30 | type: 'navigation', 31 | buffered: true 32 | }); 33 | })() 34 | ``` 35 | 36 | 37 | 38 | 39 | </details> 40 | 41 | 42 | 43 | ## Console Tab Snippet 44 | 45 | <details> 46 | 47 | <summary>Copy this code snippet into the DevTools console Tab to use it</summary> 48 | 49 | 50 | ```javascript 51 | 52 | new PerformanceObserver((entryList) => { 53 | const [pageNav] = entryList.getEntriesByType('navigation'); 54 | console.log(`TTFB (ms): ${pageNav.responseStart}`); 55 | }).observe({ 56 | type: 'navigation', 57 | buffered: true 58 | }); 59 | 60 | ``` 61 | 62 | 63 | 64 | 65 | </details> 66 | 67 | 68 | 69 | 70 | <!-- END-HOW_TO --> 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 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | -------------------------------------------------------------------------------- /snippets/time-to-first-byte-document/index.js: -------------------------------------------------------------------------------- 1 | new PerformanceObserver((entryList) => { 2 | const [pageNav] = entryList.getEntriesByType('navigation') 3 | console.log(`TTFB (ms): ${pageNav.responseStart}`) 4 | }).observe({ 5 | type: 'navigation', 6 | buffered: true 7 | }) 8 | -------------------------------------------------------------------------------- /snippets/tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "rootDir": "./" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /snippets/tsconfig.docs.json: -------------------------------------------------------------------------------- 1 | { 2 | "ts-node": { 3 | "extends": "./tsconfig.base.json", 4 | "compilerOptions": { 5 | "lib": [ 6 | "es2019", 7 | "es2020" 8 | ], 9 | "module": "commonjs", 10 | "target": "es2020" 11 | }, 12 | "include": [ 13 | "./bin.ts" 14 | ] 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /snippets/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.base.json", 3 | "compilerOptions": { 4 | "outDir": "../dist/snippets", 5 | "lib": [ 6 | "es2019", 7 | "dom" 8 | ], 9 | "module": "ES2020", 10 | "target": "ES2020", 11 | "allowJs": true 12 | }, 13 | "include": [ 14 | "./**/index.js", 15 | "./**/index.ts" 16 | ], 17 | "exclude": [ 18 | "./*.*", 19 | "_SNIPPET_TEMPLATE/**" 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /snippets/types.ts: -------------------------------------------------------------------------------- 1 | export type Technique = 'bookmark' | 'console-tab' | 'sources-tab' | 'chromium'; 2 | -------------------------------------------------------------------------------- /snippets/utils.ts: -------------------------------------------------------------------------------- 1 | import {join} from "path"; 2 | import { 3 | HOW_TO_END, 4 | HOW_TO_START, 5 | HOW_TO_START_REGEX, 6 | NEW_LINE, 7 | SNIPPETS_DIST, 8 | SNIPPETS_ROOT, 9 | SNIPPETS_TEMPLATE_NAME 10 | } from "./constants"; 11 | import {readFileSync, writeFileSync} from "fs"; 12 | import {Technique} from "./types"; 13 | 14 | function wrapCollapse(title: string, content: string): string { 15 | return `<details>${NEW_LINE} 16 | <summary>${title}</summary>${NEW_LINE} 17 | ${content}${NEW_LINE} 18 | </details>${NEW_LINE}` 19 | } 20 | 21 | export function getUsabilityTable(techniques: Technique[]): string { 22 | let table = ''; 23 | if (techniques?.length) { 24 | table += ` 25 | | Technique | Is Usable | 26 | | ----------- | ---------- | 27 | | [Bookmark](https://github.com/push-based/web-performance-tools/blob/main/docs/how-to-use-it-with-bookmarks) | ${techniques.includes('bookmark') ? '✔' : '❌'} | 28 | | [Console Tab](https://github.com/push-based/web-performance-tools/blob/main/docs/how-to-use-it-with-console-tab.md) | ${techniques.includes('console-tab') ? '✔' : '❌'} | 29 | | [Sources Tab](https://github.com/push-based/web-performance-tools/blob/main/docs/how-to-use-it-with-sources-tab.md) | ${techniques.includes('sources-tab') ? '✔' : '❌'} | 30 | | [Chromium](https://github.com/push-based/web-performance-tools/blob/main/docs/how-to-use-it-with-chromium.md) | ${techniques.includes('chromium') ? '✔' : '❌'} | 31 | ` 32 | } 33 | return table + NEW_LINE + NEW_LINE; 34 | } 35 | 36 | function wrapBookmarkIIFE(js: string): string { 37 | return `javascript:(() => {${js}})()`; 38 | } 39 | 40 | function wrapJsMd(js: string): string { 41 | return ` 42 | \`\`\`javascript${NEW_LINE} 43 | ${js} 44 | \`\`\` ${NEW_LINE} 45 | ${NEW_LINE}`; 46 | } 47 | 48 | export function toBookmarkSnippet(c: string, cfg?: { h: string }): string { 49 | const h = cfg?.h || '###'; 50 | // @TODO add compression 51 | return ` 52 | ${h} Bookmark Snippet${NEW_LINE} 53 | ${NEW_LINE} 54 | ${wrapCollapse('Copy this code snippet into the bookmark to use it', wrapJsMd(wrapBookmarkIIFE(c)))} 55 | ${NEW_LINE} 56 | `; 57 | } 58 | 59 | export function toConsoleSnippet(c: string): string { 60 | return ` 61 | ${wrapCollapse('Copy this code snippet into the DevTools console Tab to use it', wrapJsMd(c 62 | // @TODO the replace is a hack that should be fixed in the snippets:compile step in the tsconfig.json 63 | .replace('export {};', '') 64 | ))} 65 | ${NEW_LINE} 66 | `; 67 | } 68 | 69 | export function updateSnippet(folder: string): void { 70 | const readmePath = join(SNIPPETS_ROOT, folder, 'Readme.md'); 71 | const snippetPath = join(SNIPPETS_DIST, folder, 'index.js'); 72 | 73 | let readmeContent: string = readFileSync(readmePath, 'utf8'); 74 | if (readmeContent.trim() === '') { 75 | throw new Error(`${readmePath} is missing. Please use the snippet from ${SNIPPETS_TEMPLATE_NAME}`) 76 | } 77 | let snippetContent: string = readFileSync(snippetPath, 'utf8'); 78 | if (snippetContent.trim() === '') { 79 | throw new Error(`${snippetPath} is missing. Did you run 'npm run snippets:compile'`) 80 | } 81 | 82 | const howToMatch = HOW_TO_START_REGEX.exec(readmeContent); 83 | const techniques = howToMatch ? howToMatch[2].split(',') as Technique[] : []; 84 | 85 | const table = getUsabilityTable(techniques); 86 | const bookmark = toBookmarkSnippet(snippetContent); 87 | const ch2 = `## Console Tab Snippet` + NEW_LINE; 88 | const consoleTab = toConsoleSnippet(snippetContent); 89 | let out = table + bookmark + ch2 + consoleTab + NEW_LINE; 90 | 91 | if (out !== '') { 92 | const [start, _s, _t, _e, rest_] = readmeContent.split(HOW_TO_START_REGEX) || []; 93 | if (rest_ === undefined) { 94 | throw new Error(`${readmePath} is missing the comments: "${HOW_TO_START(techniques)}" and ${HOW_TO_END}`) 95 | } 96 | const [_oldContent, end] = rest_?.split(HOW_TO_END); 97 | out = start + 98 | HOW_TO_START(techniques) + NEW_LINE + NEW_LINE + 99 | out + 100 | HOW_TO_END + NEW_LINE + NEW_LINE + 101 | end; 102 | writeFileSync(readmePath, out, 'utf8'); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /time-stats/README.md: -------------------------------------------------------------------------------- 1 | ## time-stats 2 | * `npm i` 3 | * Put json files generated by chrome profiler to this folder 4 | * Specify their names in `time-stats/index.js` 5 | * Run `npm run time-stats` 6 | ![img](images/img.png) 7 | -------------------------------------------------------------------------------- /time-stats/images/img.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/push-based/awesome-web-performance-snippets/2273b69d62e12f44520a5d61cf8cd6c1fa6131b6/time-stats/images/img.png -------------------------------------------------------------------------------- /time-stats/index.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const _ = require('lodash') 3 | 4 | const allKey = 'all' 5 | 6 | function getUrlTimeMap (filePath, eventName) { 7 | const file = fs.readFileSync(__dirname + '/' + filePath, { encoding: 'utf8' }) 8 | const events = JSON.parse(file) 9 | const filteredEvents = events.filter(e => e.name === eventName) 10 | return createUrlMap(filteredEvents, eventName) 11 | } 12 | 13 | function createUrlMap (events, defaultKeyName) { 14 | const map = { [allKey]: [] } 15 | 16 | events.forEach(t => { 17 | const key = t.args.data.url || defaultKeyName 18 | const dur = t.dur || 0 19 | if (!dur) {return} 20 | if (!map[key]) { 21 | map[key] = [dur] 22 | } else { 23 | map[key].push(dur) 24 | } 25 | map[allKey].push(dur) 26 | }) 27 | return _.mapValues(map, value => +(_.mean(value).toFixed(0))) 28 | } 29 | 30 | function compare (firstPath, secondPath, eventName, showTotal = true) { 31 | const map1 = getUrlTimeMap(firstPath, eventName) 32 | const map2 = getUrlTimeMap(secondPath, eventName) 33 | const diffLabel = 'diff(%)' 34 | let resultsTable = []; 35 | _.uniq([...Object.keys(map1), ...Object.keys(map2)]).forEach(key => { 36 | if (key === allKey && !showTotal) { 37 | return 38 | } 39 | const val1 = map1[key] 40 | const val2 = map2[key] 41 | if (val1 && val2) { 42 | const diff = +((val2 / val1) * 100).toFixed(0) 43 | resultsTable.push({ url: key.substring(0, 80), [firstPath]: val1, [secondPath]: val2, [diffLabel]: diff }) 44 | } 45 | }) 46 | resultsTable.sort((a, b) => b.url === allKey ? 0 : b[diffLabel] - a[diffLabel]) 47 | 48 | console.table(resultsTable) 49 | } 50 | 51 | function compareXHR (first, second) { 52 | compare('xhr/' + first, 'xhr/' + second, 'XHRLoad') 53 | } 54 | 55 | function compareTimer (first, second) { 56 | compare('timer/' + first, 'timer/' + second, 'TimerFire', false) 57 | } 58 | 59 | compareXHR('xhr_flags_set.json', 'xhr_full_zone.json') 60 | compareTimer('timers_flag_set.json', 'timers_full_zone.json') 61 | --------------------------------------------------------------------------------