generated from adobe/aem-boilerplate
-
Notifications
You must be signed in to change notification settings - Fork 175
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Added performance tab to the Preflight tool (#2773)
added performance tab
- Loading branch information
1 parent
ec48f3c
commit 74fcdc4
Showing
8 changed files
with
631 additions
and
4 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,271 @@ | ||
import { html, signal, useEffect } from '../../../deps/htm-preact.js'; | ||
import { getMetadata } from '../../../utils/utils.js'; | ||
|
||
const icons = { | ||
pass: 'green', | ||
fail: 'red', | ||
empty: 'empty', | ||
}; | ||
// TODO MEP/Personalization | ||
// TODO mobile / tablet? | ||
// TODO create ticket for PSI API | ||
// TODO link to documentation directly within the sections | ||
const text = { | ||
lcpEl: { | ||
key: 'lcpEl', | ||
title: 'Valid LCP', | ||
passed: { description: 'Valid LCP in the first section detected.' }, | ||
failed: { description: 'No LCP image or video in the first section detected.' }, | ||
}, | ||
singleBlock: { | ||
key: 'singleBlock', | ||
title: 'Single Block', | ||
passed: { description: 'First section has exactly one block.' }, | ||
failed: { description: 'First section has more than one block.' }, | ||
}, | ||
imageSize: { | ||
key: 'imageSize', | ||
title: 'Images size', | ||
empty: { description: 'No image as LCP element.' }, | ||
passed: { description: 'LCP image is less than 100KB.' }, | ||
failed: { description: 'LCP image is over 100KB.' }, | ||
}, | ||
videoPoster: { | ||
key: 'videoPoster', | ||
title: 'Videos', | ||
empty: { description: 'No video as LCP element.' }, | ||
passed: { description: 'LCP video has a poster attribute' }, | ||
failed: { description: 'LCP video has no poster attribute.' }, | ||
}, | ||
fragments: { | ||
key: 'fragments', | ||
title: 'Fragments', | ||
passed: { description: 'No fragments used within the LCP section.' }, | ||
failed: { description: 'Fragments used within the LCP section.' }, | ||
}, | ||
personalization: { | ||
key: 'personalization', | ||
title: 'Personalization', | ||
passed: { description: 'Personalization is currently not enabled.' }, | ||
failed: { description: 'MEP or Target enabled.' }, | ||
}, | ||
placeholders: { | ||
key: 'placeholders', | ||
title: 'Placeholders', | ||
passed: { description: 'No placeholders found within the LCP section.' }, | ||
failed: { description: 'Placeholders found within the LCP section.' }, | ||
}, | ||
icons: { | ||
key: 'icons', | ||
title: 'Icons', | ||
passed: { description: 'No icons found within the LCP section.' }, | ||
failed: { description: 'Icons found within the LCP section.' }, | ||
}, | ||
}; | ||
|
||
export const config = { | ||
items: signal([ | ||
...Object.values(text).map(({ key, title }) => ({ | ||
key, | ||
title, | ||
icon: icons.empty, | ||
description: 'Loading...', | ||
})), | ||
]), | ||
lcp: null, | ||
cls: 0, | ||
}; | ||
|
||
export const findItem = (key) => config.items.value.find((item) => item.key === key); | ||
export const updateItem = ({ key, ...updates }) => { | ||
const { items } = config; | ||
items.value = items.value.map((item) => (item.key === key ? { ...item, ...updates } : item)); | ||
}; | ||
|
||
function observePerfMetrics() { | ||
// Observe LCP | ||
const lcpObserver = new PerformanceObserver((entryList) => { | ||
const entries = entryList.getEntries(); | ||
const lastEntry = entries[entries.length - 1]; | ||
if (!lastEntry || lastEntry.url.includes('preflight')) return; | ||
config.lcp = lastEntry; | ||
}); | ||
lcpObserver.observe({ type: 'largest-contentful-paint', buffered: true }); | ||
|
||
// Observe CLS | ||
const clsObserver = new PerformanceObserver((entryList) => { | ||
const entries = entryList.getEntries(); | ||
entries.forEach((entry) => { | ||
if (!entry.hadRecentInput) { | ||
config.cls += entry.value; | ||
} | ||
}); | ||
if (config.cls > 0) { | ||
// TODO - Lana log? We should not have any CLS. | ||
// console.log('CLS:', cls); | ||
} | ||
}); | ||
clsObserver.observe({ type: 'layout-shift', buffered: true }); | ||
} | ||
|
||
// Dont call the method to retain the empty state of an item | ||
export function conditionalItemUpdate({ emptyWhen, failsWhen, key }) { | ||
if (emptyWhen) { | ||
updateItem({ | ||
key, | ||
icon: icons.empty, | ||
description: text[key].empty.description, | ||
}); | ||
return; | ||
} | ||
if (failsWhen) { | ||
updateItem({ | ||
key, | ||
icon: icons.fail, | ||
description: text[key].failed.description, | ||
}); | ||
return; | ||
} | ||
updateItem({ | ||
key, | ||
icon: icons.pass, | ||
description: text[key].passed.description, | ||
}); | ||
} | ||
|
||
// TODO do we also want to check the content-length header? | ||
// https://www.w3.org/TR/largest-contentful-paint/#largest-contentful-paint-candidate-element | ||
// candidate’s element is a text node, or candidate’s request's response's content length | ||
// in bytes is >= candidate’s element's effective visual size * 0.004 | ||
export async function checkImageSize() { | ||
const { lcp } = config; | ||
const hasValidImage = lcp?.url && !lcp.url.match('media_.*.mp4'); | ||
let blob; | ||
let isSizeValid; | ||
if (hasValidImage) { | ||
blob = await fetch(lcp.url).then((res) => res.blob()); | ||
isSizeValid = blob.size / 1000 <= 100; | ||
} | ||
|
||
conditionalItemUpdate({ | ||
failsWhen: !isSizeValid, | ||
emptyWhen: !hasValidImage, | ||
key: text.imageSize.key, | ||
}); | ||
} | ||
|
||
export async function checkLCP() { | ||
const { lcp } = config; | ||
const mainSection = document.querySelector('main > div.section'); | ||
conditionalItemUpdate({ | ||
failsWhen: !lcp || !lcp.element || !lcp.url || !mainSection?.contains(lcp.element), | ||
key: text.lcpEl.key, | ||
icon: icons.pass, | ||
description: text.lcpEl.passed.description, | ||
}); | ||
} | ||
|
||
export const checkFragments = () => conditionalItemUpdate({ | ||
failsWhen: config.lcp.element.closest('.fragment') || config.lcp.element.closest('.section')?.querySelector('[data-path*="fragment"]'), | ||
key: text.fragments.key, | ||
}); | ||
|
||
export const checkPlaceholders = async () => conditionalItemUpdate({ | ||
failsWhen: config.lcp.element.closest('.section').dataset.placeholdersDecorated === 'true', | ||
key: text.placeholders.key, | ||
}); | ||
|
||
export const checkForPersonalization = async () => conditionalItemUpdate({ | ||
failsWhen: getMetadata('personalization') || getMetadata('target') === 'on', | ||
key: text.personalization.key, | ||
}); | ||
|
||
export const checkVideosWithoutPosterAttribute = async () => conditionalItemUpdate({ | ||
failsWhen: !config.lcp.element.poster, | ||
emptyWhen: !config.lcp.url.match('media_.*.mp4'), | ||
key: text.videoPoster.key, | ||
}); | ||
|
||
export const checkIcons = () => conditionalItemUpdate({ | ||
failsWhen: config.lcp.element.closest('.section').querySelector('.icon-milo'), | ||
key: text.icons.key, | ||
}); | ||
|
||
export function PerformanceItem({ icon, title, description }) { | ||
return html` <div class="seo-item"> | ||
<div class="result-icon ${icon}"></div> | ||
<div class="seo-item-text"> | ||
<p class="seo-item-title">${title}</p> | ||
<p class="seo-item-description">${description}</p> | ||
</div> | ||
</div>`; | ||
} | ||
|
||
export const checkForSingleBlock = () => conditionalItemUpdate({ | ||
failsWhen: document.querySelector('main > div.section').childElementCount > 1, | ||
key: text.singleBlock.key, | ||
}); | ||
|
||
async function getResults() { | ||
observePerfMetrics(); | ||
checkLCP(); | ||
checkFragments(); | ||
checkForPersonalization(); | ||
checkVideosWithoutPosterAttribute(); | ||
checkIcons(); | ||
checkForSingleBlock(); | ||
await Promise.all([checkImageSize(), checkPlaceholders()]); | ||
} | ||
|
||
export const createPerformanceItem = ({ | ||
icon, | ||
title, | ||
description, | ||
} = {}) => html`<${PerformanceItem} | ||
icon=${icon} | ||
title=${title} | ||
description=${description} | ||
/>`; | ||
|
||
let clonedLcpSection; | ||
function highlightElement(event) { | ||
const lcpSection = config.lcp.element.closest('.section'); | ||
const tooltip = document.querySelector('.lcp-tooltip-modal'); | ||
const { offsetHeight, offsetWidth } = lcpSection; | ||
const scaleFactor = Math.min(500 / offsetWidth, 500 / offsetHeight); | ||
if (!clonedLcpSection) { | ||
clonedLcpSection = lcpSection.cloneNode(true); | ||
clonedLcpSection.classList.add('lcp-clone'); | ||
clonedLcpSection.style.width = `${lcpSection.offsetWidth}px`; | ||
clonedLcpSection.style.height = `${lcpSection.offsetHeight}px`; | ||
clonedLcpSection.style.transform = `scale(${scaleFactor})`; | ||
clonedLcpSection.style.transformOrigin = 'top left'; | ||
} | ||
if (!tooltip.children.length) tooltip.appendChild(clonedLcpSection); | ||
const { top, left } = event.currentTarget.getBoundingClientRect(); | ||
tooltip.style.width = `${offsetWidth * scaleFactor}px`; | ||
tooltip.style.height = `${offsetHeight * scaleFactor}px`; | ||
tooltip.style.top = `${top + window.scrollY - offsetHeight * scaleFactor - 10}px`; | ||
tooltip.style.left = `${left + window.scrollX}px`; | ||
document.querySelector('.lcp-tooltip-modal').classList.add('show'); | ||
} | ||
|
||
const removeHighlight = () => { document.querySelector('.lcp-tooltip-modal').classList.remove('show'); }; | ||
|
||
export function Panel() { | ||
useEffect(() => { | ||
getResults(); | ||
}, []); | ||
|
||
return html` | ||
<div class="seo-columns"> | ||
<div class="seo-column">${config.items.value.slice(0, 4).map((item) => createPerformanceItem(item))}</div> | ||
<div class="seo-column">${config.items.value.slice(4, 8).map((item) => createPerformanceItem(item))}</div> | ||
<div>Unsure on how to get this page fully into the green? Check out the <a class="performance-guidelines" href="https://milo.adobe.com/docs/authoring/performance/" target="_blank">Milo Performance Guidelines</a>.</div> | ||
<div> <span class="performance-element-preview" onMouseEnter=${highlightElement} onMouseLeave=${removeHighlight}>Highlight the found LCP section</span> </div> | ||
</div> | ||
<div class="lcp-tooltip-modal"></div> | ||
`; | ||
} | ||
|
||
export default Panel; |
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
30 changes: 30 additions & 0 deletions
30
test/blocks/preflight/panels/performance/mocks/marquee.html
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,30 @@ | ||
<main> | ||
<div> | ||
<div class="marquee dark"> | ||
<div> | ||
<div> | ||
<br> | ||
<picture> | ||
<source type="image/webp" srcset="./media_1e3592d2ec5d63ce4a4d8a920d87da94b5b2cee5c.png?width=2000&format=webply&optimize=medium" media="(min-width: 600px)"> | ||
<source type="image/webp" srcset="./media_1e3592d2ec5d63ce4a4d8a920d87da94b5b2cee5c.png?width=750&format=webply&optimize=medium"> | ||
<source type="image/png" srcset="./media_1e3592d2ec5d63ce4a4d8a920d87da94b5b2cee5c.png?width=2000&format=png&optimize=medium" media="(min-width: 600px)"> | ||
<img loading="lazy" alt="" src="./media_1e3592d2ec5d63ce4a4d8a920d87da94b5b2cee5c.png?width=750&format=png&optimize=medium" width="1024" height="1024"> | ||
</picture> | ||
</div> | ||
</div> | ||
<div> | ||
<div> | ||
<h1 id="using-data-to-transform-attitudes-and-evolve-experiences">Using data to transform attitudes and evolve experiences.</h1> | ||
<p>How BMW Group transformed the online automotive experience for customers, dealers, and employees</p> | ||
<p> | ||
<strong> | ||
<em> | ||
<a href="/fragments/customer-success-stories/modals/bmw#watch-now">Watch Now</a> | ||
</em> | ||
</strong> | ||
</p> | ||
</div> | ||
</div> | ||
</div> | ||
</div> | ||
</main> |
Oops, something went wrong.