-
-
Notifications
You must be signed in to change notification settings - Fork 2.6k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(audits): Add initial perf audits (#10015)
* feat(audits): Add initial perf audits * feat(audits): Setup dev astro-island * fix(audits): Don't take scroll into account when getting an element's position * nit: lint * Fix tests * chore: changeset * maybe: Move this.hydrator outside the perf check * Update packages/astro/e2e/dev-toolbar.test.js Co-authored-by: Bjorn Lu <bjornlu.dev@gmail.com> * address feedback * address feedback --------- Co-authored-by: Bjorn Lu <bjornlu.dev@gmail.com>
- Loading branch information
1 parent
f9aebe7
commit 6884b10
Showing
18 changed files
with
309 additions
and
41 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
"astro": minor | ||
--- | ||
|
||
Adds initial support for performance audits to the dev toolbar |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
import { expect } from '@playwright/test'; | ||
import { testFactory } from './test-utils.js'; | ||
|
||
const test = testFactory({ | ||
root: './fixtures/dev-toolbar/', | ||
}); | ||
|
||
let devServer; | ||
|
||
test.beforeAll(async ({ astro }) => { | ||
devServer = await astro.startDevServer(); | ||
}); | ||
|
||
test.afterAll(async () => { | ||
await devServer.stop(); | ||
}); | ||
|
||
test.describe('Dev Toolbar - Audits', () => { | ||
test('can warn about perf issues zzz', async ({ page, astro }) => { | ||
await page.goto(astro.resolveUrl('/audits-perf')); | ||
|
||
const toolbar = page.locator('astro-dev-toolbar'); | ||
const appButton = toolbar.locator('button[data-app-id="astro:audit"]'); | ||
await appButton.click(); | ||
|
||
const auditCanvas = toolbar.locator('astro-dev-toolbar-app-canvas[data-app-id="astro:audit"]'); | ||
const auditHighlights = auditCanvas.locator('astro-dev-toolbar-highlight'); | ||
|
||
const count = await auditHighlights.count(); | ||
expect(count).toEqual(2); | ||
|
||
for (const auditHighlight of await auditHighlights.all()) { | ||
await expect(auditHighlight).toBeVisible(); | ||
|
||
const auditCode = await auditHighlight.getAttribute('data-audit-code'); | ||
expect(auditCode.startsWith('perf-')).toBe(true); | ||
|
||
await auditHighlight.hover(); | ||
const auditHighlightTooltip = auditHighlight.locator('astro-dev-toolbar-tooltip'); | ||
await expect(auditHighlightTooltip).toBeVisible(); | ||
} | ||
|
||
// Toggle app off | ||
await appButton.click(); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Binary file not shown.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
10 changes: 10 additions & 0 deletions
10
packages/astro/e2e/fixtures/dev-toolbar/src/pages/audits-perf.astro
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
--- | ||
import { Image } from "astro:assets"; | ||
import walrus from "../light_walrus.avif"; | ||
--- | ||
|
||
<Image src={walrus} loading="lazy" alt="A walrus" /> | ||
|
||
<div style="height: 9000px;"></div> | ||
|
||
<Image src={walrus} loading="eager" alt="A walrus" /> |
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
125 changes: 125 additions & 0 deletions
125
packages/astro/src/runtime/client/dev-toolbar/apps/audit/perf.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,125 @@ | ||
import type { AuditRuleWithSelector } from './index.js'; | ||
|
||
// A regular expression to match external URLs | ||
const EXTERNAL_URL_REGEX = /^(?:[a-z+]+:)?\/\//i; | ||
|
||
export const perf: AuditRuleWithSelector[] = [ | ||
{ | ||
code: 'perf-use-image-component', | ||
title: 'Use the Image component', | ||
message: 'This image could be replaced with the Image component to improve performance.', | ||
selector: 'img:not([data-image-component])', | ||
async match(element) { | ||
const src = element.getAttribute('src'); | ||
if (!src) return false; | ||
|
||
// Don't match data URIs, they're typically used for specific use-cases that the image component doesn't help with | ||
if (src.startsWith('data:')) return false; | ||
|
||
// Ignore images that are smaller than 20KB, most of the time the image component won't really help with these, or they're used for specific use-cases (pixel tracking, etc.) | ||
// Ignore this test for remote images for now, fetching them can be very slow and possibly dangerous | ||
if (!EXTERNAL_URL_REGEX.test(src)) { | ||
const imageData = await fetch(src).then((response) => response.blob()); | ||
if (imageData.size < 20480) return false; | ||
} | ||
|
||
return true; | ||
}, | ||
}, | ||
{ | ||
code: 'perf-use-loading-lazy', | ||
title: 'Use the loading="lazy" attribute', | ||
message: (element) => | ||
`This ${element.nodeName} tag is below the fold and could be lazy-loaded to improve performance.`, | ||
selector: | ||
'img:not([loading]), img[loading="eager"], iframe:not([loading]), iframe[loading="eager"]', | ||
match(element) { | ||
const htmlElement = element as HTMLImageElement | HTMLIFrameElement; | ||
// Ignore elements that are above the fold, they should be loaded eagerly | ||
if (htmlElement.offsetTop < window.innerHeight) return false; | ||
|
||
return true; | ||
}, | ||
}, | ||
{ | ||
code: 'perf-use-loading-eager', | ||
title: 'Use the loading="eager" attribute', | ||
message: (element) => | ||
`This ${element.nodeName} tag is above the fold and could be eagerly-loaded to improve performance.`, | ||
selector: 'img[loading="lazy"], iframe[loading="lazy"]', | ||
match(element) { | ||
const htmlElement = element as HTMLImageElement | HTMLIFrameElement; | ||
|
||
// Ignore elements that are below the fold, they should be loaded lazily | ||
if (htmlElement.offsetTop > window.innerHeight) return false; | ||
|
||
return true; | ||
}, | ||
}, | ||
{ | ||
code: 'perf-use-videos', | ||
title: 'Use videos instead of GIFs for large animations', | ||
message: | ||
'This GIF could be replaced with a video to reduce its file size and improve performance.', | ||
selector: 'img[src$=".gif"]', | ||
async match(element) { | ||
const src = element.getAttribute('src'); | ||
if (!src) return false; | ||
|
||
// Ignore remote URLs | ||
if (EXTERNAL_URL_REGEX.test(src)) return false; | ||
|
||
// Ignore GIFs that are smaller than 100KB, those are typically small enough to not be a problem | ||
if (!EXTERNAL_URL_REGEX.test(src)) { | ||
const imageData = await fetch(src).then((response) => response.blob()); | ||
if (imageData.size < 102400) return false; | ||
} | ||
|
||
return true; | ||
}, | ||
}, | ||
{ | ||
code: 'perf-slow-component-server-render', | ||
title: 'Server-rendered component took a long time to render', | ||
message: (element) => | ||
`This component took an unusually long time to render on the server (${getCleanRenderingTime( | ||
element.getAttribute('server-render-time') | ||
)}). This might be a sign that it's doing too much work on the server, or something is blocking rendering.`, | ||
selector: 'astro-island[server-render-time]', | ||
match(element) { | ||
const serverRenderTime = element.getAttribute('server-render-time'); | ||
if (!serverRenderTime) return false; | ||
|
||
const renderingTime = parseFloat(serverRenderTime); | ||
if (Number.isNaN(renderingTime)) return false; | ||
|
||
return renderingTime > 500; | ||
}, | ||
}, | ||
{ | ||
code: 'perf-slow-component-client-hydration', | ||
title: 'Client-rendered component took a long time to hydrate', | ||
message: (element) => | ||
`This component took an unusually long time to render on the server (${getCleanRenderingTime( | ||
element.getAttribute('client-render-time') | ||
)}). This could be a sign that something is blocking the main thread and preventing the component from hydrating quickly.`, | ||
selector: 'astro-island[client-render-time]', | ||
match(element) { | ||
const clientRenderTime = element.getAttribute('client-render-time'); | ||
if (!clientRenderTime) return false; | ||
|
||
const renderingTime = parseFloat(clientRenderTime); | ||
if (Number.isNaN(renderingTime)) return false; | ||
|
||
return renderingTime > 500; | ||
}, | ||
}, | ||
]; | ||
|
||
function getCleanRenderingTime(time: string | null) { | ||
if (!time) return 'unknown'; | ||
const renderingTime = parseFloat(time); | ||
if (Number.isNaN(renderingTime)) return 'unknown'; | ||
|
||
return renderingTime.toFixed(2) + 's'; | ||
} |
Oops, something went wrong.