-
Notifications
You must be signed in to change notification settings - Fork 226
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
LCP & Above the Fold Optimization (#6311)
Co-authored-by: Michael Lee <[email protected]> Co-authored-by: Gael Robin <[email protected]> Co-authored-by: Opeyemi Ibrahim <[email protected]> Co-authored-by: Mathieu Lamiot <[email protected]> Co-authored-by: WordPressFan <[email protected]>
- Loading branch information
1 parent
0aed49b
commit afe019d
Showing
249 changed files
with
10,594 additions
and
4,643 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,315 @@ | ||
class RocketLcpBeacon { | ||
constructor( config ) { | ||
this.config = config; | ||
this.performanceImages = []; | ||
} | ||
|
||
init() { | ||
if ( ! this._isValidPreconditions() ) { | ||
this._finalize(); | ||
return; | ||
} | ||
|
||
// Use _generateLcpCandidates method to get all the elements in the viewport. | ||
const above_the_fold_images = this._generateLcpCandidates( Infinity ); | ||
if ( above_the_fold_images ) { | ||
this._initWithFirstElementWithInfo( above_the_fold_images ); | ||
this._fillATFWithoutDuplications( above_the_fold_images ); | ||
} | ||
|
||
this._saveFinalResultIntoDB(); | ||
} | ||
|
||
_isValidPreconditions() { | ||
// Check the screensize first because starting any logic. | ||
if ( this._isNotValidScreensize() ) { | ||
this._logMessage('Bailing out because screen size is not acceptable'); | ||
return false; | ||
} | ||
|
||
if ( this._isGeneratedBefore() ) { | ||
this._logMessage('Bailing out because data is already available'); | ||
return false; | ||
} | ||
|
||
return true; | ||
} | ||
|
||
_isGeneratedBefore() { | ||
// AJAX call to check if there are any records for the current URL. | ||
let data_check = new FormData(); | ||
data_check.append('action', 'rocket_check_lcp'); | ||
data_check.append('rocket_lcp_nonce', this.config.nonce); | ||
data_check.append('url', this.config.url); | ||
data_check.append('is_mobile', this.config.is_mobile); | ||
|
||
const lcp_data_response = fetch(this.config.ajax_url, { | ||
method: "POST", | ||
credentials: 'same-origin', | ||
body: data_check | ||
}); | ||
|
||
return lcp_data_response.success; | ||
} | ||
|
||
_isNotValidScreensize() { | ||
// Check screen size | ||
const screenWidth = window.innerWidth || document.documentElement.clientWidth; | ||
const screenHeight= window.innerHeight || document.documentElement.clientHeight; | ||
|
||
const isNotValidForMobile = this.config.is_mobile && | ||
( screenWidth > this.config.width_threshold || screenHeight > this.config.height_threshold ); | ||
const isNotValidForDesktop = !this.config.is_mobile && | ||
( screenWidth < this.config.width_threshold || screenHeight < this.config.height_threshold ); | ||
|
||
return isNotValidForMobile || isNotValidForDesktop; | ||
} | ||
|
||
_generateLcpCandidates( count ) { | ||
const lcpElements = document.querySelectorAll( this.config.elements ); | ||
|
||
if ( lcpElements.length <= 0 ) { | ||
return []; | ||
} | ||
|
||
const potentialCandidates = Array.from( lcpElements ); | ||
|
||
const topCandidates = potentialCandidates.map(element => { | ||
const rect = element.getBoundingClientRect(); | ||
return { | ||
element: element, | ||
rect: rect, | ||
}; | ||
}).filter(item => { | ||
return ( | ||
item.rect.width > 0 && | ||
item.rect.height > 0 && | ||
this._isIntersecting(item.rect) | ||
); | ||
}) | ||
.map(item => ({ | ||
item, | ||
area: this._getElementArea(item.rect), | ||
elementInfo: this._getElementInfo(item.element), | ||
})) | ||
.sort((a, b) => b.area - a.area) | ||
.slice(0, count); | ||
|
||
return topCandidates.map(candidate => ({ | ||
element: candidate.item.element, | ||
elementInfo: candidate.elementInfo, | ||
})); | ||
} | ||
|
||
_isIntersecting(rect) { | ||
// Check if any part of the image is within the viewport | ||
return ( | ||
rect.bottom >= 0 && | ||
rect.right >= 0 && | ||
rect.top <= (window.innerHeight || document.documentElement.clientHeight) && | ||
rect.left <= (window.innerWidth || document.documentElement.clientWidth) | ||
); | ||
} | ||
|
||
_getElementArea(rect) { | ||
const visibleWidth = Math.min(rect.width, (window.innerWidth || document.documentElement.clientWidth) - rect.left); | ||
const visibleHeight = Math.min(rect.height, (window.innerHeight || document.documentElement.clientHeight) - rect.top); | ||
|
||
return visibleWidth * visibleHeight; | ||
} | ||
|
||
_getElementInfo(element) { | ||
const nodeName = element.nodeName.toLowerCase(); | ||
const element_info = { | ||
type: "", | ||
src: "", | ||
srcset: "", | ||
sizes: "", | ||
sources: [], | ||
bg_set: [], | ||
current_src: "" | ||
}; | ||
|
||
const css_bg_url_rgx = /url\(\s*?['"]?\s*?(\S+?)\s*?["']?\s*?\)\s*?([a-zA-Z0-9\s]*[x|dpcm|dpi|dppx]?)/ig; | ||
|
||
if (nodeName === "img" && element.srcset) { | ||
element_info.type = "img-srcset"; | ||
element_info.src = element.src; | ||
element_info.srcset = element.srcset; // capture srcset | ||
element_info.sizes = element.sizes; // capture sizes | ||
element_info.current_src = element.currentSrc; | ||
} else if (nodeName === "img") { | ||
element_info.type = "img"; | ||
element_info.src = element.src; | ||
element_info.current_src = element.currentSrc; | ||
} else if (nodeName === "video") { | ||
element_info.type = "img"; | ||
const source = element.querySelector('source'); | ||
element_info.src = element.poster || (source ? source.src : ''); | ||
element_info.current_src = element_info.src; | ||
} else if (nodeName === "svg") { | ||
const imageElement = element.querySelector('image'); | ||
if (imageElement) { | ||
element_info.type = "img"; | ||
element_info.src = imageElement.getAttribute('href') || ''; | ||
element_info.current_src = element_info.src; | ||
} | ||
} else if (nodeName === "picture") { | ||
element_info.type = "picture"; | ||
const img = element.querySelector('img:not(picture>img)'); | ||
element_info.src = img ? img.src : ""; | ||
element_info.sources = Array.from(element.querySelectorAll('source')).map(source => ({ | ||
srcset: source.srcset || '', | ||
media: source.media || '' | ||
})); | ||
} else { | ||
const computed_style = window.getComputedStyle(element, null); | ||
const bg_props = [ | ||
computed_style.getPropertyValue("background-image"), | ||
getComputedStyle(element, ":after").getPropertyValue("background-image"), | ||
getComputedStyle(element, ":before").getPropertyValue("background-image") | ||
]; | ||
|
||
const full_bg_prop = bg_props.filter(prop => prop !== "none").join(""); | ||
element_info.type = "bg-img"; | ||
if (full_bg_prop.includes("image-set(")) { | ||
element_info.type = "bg-img-set"; | ||
} | ||
if (!full_bg_prop || full_bg_prop === "") { | ||
return null; | ||
} | ||
|
||
const matches = [...full_bg_prop.matchAll(css_bg_url_rgx)]; | ||
element_info.bg_set = matches.map(m => m[1] ? {src: m[1].trim() + (m[2] ? " " + m[2].trim() : "")} : {}); | ||
// Check if bg_set array is populated with empty objects | ||
if (element_info.bg_set.every(item => item.src === "")) { | ||
// If bg_set array is populated with empty objects, populate it with the URLs from the matches array | ||
element_info.bg_set = matches.map(m => m[1] ? {src: m[1].trim()} : {}); | ||
} | ||
|
||
if (element_info.bg_set.length > 0) { | ||
element_info.src = element_info.bg_set[0].src; | ||
if (element_info.type === "bg-img-set") { | ||
element_info.src = element_info.bg_set; | ||
} | ||
} | ||
} | ||
|
||
return element_info; | ||
} | ||
|
||
_initWithFirstElementWithInfo(elements) { | ||
const firstElementWithInfo = elements.find(item => item.elementInfo !== null); | ||
|
||
if ( ! firstElementWithInfo ) { | ||
this._logMessage("No LCP candidate found."); | ||
this.performanceImages = []; | ||
return; | ||
} | ||
|
||
this.performanceImages = [{ | ||
...firstElementWithInfo.elementInfo, | ||
label: "lcp", | ||
}]; | ||
} | ||
|
||
_fillATFWithoutDuplications(elements) { | ||
elements.forEach(({ element, elementInfo }) => { | ||
if ( this._isDuplicateImage(element) ) { | ||
return; | ||
} | ||
|
||
this.performanceImages.push({ ...elementInfo, label: "above-the-fold" }); | ||
}); | ||
} | ||
|
||
_isDuplicateImage(image) { | ||
const elementInfo = this._getElementInfo(image); | ||
|
||
if (elementInfo === null) { | ||
return false; | ||
} | ||
|
||
const isImageOrVideo = | ||
elementInfo.type === "img" || | ||
elementInfo.type === "img-srcset" || | ||
elementInfo.type === "video"; | ||
|
||
const isBgImageOrPicture = | ||
elementInfo.type === "bg-img" || | ||
elementInfo.type === "bg-img-set" || | ||
elementInfo.type === "picture"; | ||
|
||
return (isImageOrVideo || isBgImageOrPicture) | ||
&& | ||
this.performanceImages.some(item => item.src === elementInfo.src); | ||
} | ||
|
||
_saveFinalResultIntoDB() { | ||
if ( ! this.performanceImages ) { | ||
return; | ||
} | ||
|
||
const data = new FormData(); | ||
data.append('action', 'rocket_lcp'); | ||
data.append('rocket_lcp_nonce', this.config.nonce); | ||
data.append('url', this.config.url); | ||
data.append('is_mobile', this.config.is_mobile); | ||
data.append('images', JSON.stringify(this.performanceImages)); | ||
data.append('status', 'success'); | ||
|
||
fetch(rocket_lcp_data.ajax_url, { | ||
method: "POST", | ||
credentials: 'same-origin', | ||
body: data, | ||
headers: { | ||
'wpr-saas-no-intercept': true | ||
} | ||
}) | ||
.then((response) => response.json()) | ||
.then((data) => { | ||
this._logMessage(data); | ||
}) | ||
.catch((error) => { | ||
this._logMessage(error); | ||
}) | ||
.finally(() => { | ||
this._finalize(); | ||
}); | ||
} | ||
|
||
_finalize() { | ||
const beaconscript = document.querySelector('[data-name="wpr-lcp-beacon"]'); | ||
beaconscript.setAttribute('beacon-completed', 'true'); | ||
} | ||
|
||
_logMessage( msg ) { | ||
if ( ! this.config.debug ) { | ||
return; | ||
} | ||
console.log( msg ); | ||
} | ||
|
||
static run() { | ||
if ( !window.rocket_lcp_data ) { | ||
return; | ||
} | ||
|
||
const instance = new RocketLcpBeacon( window.rocket_lcp_data ); | ||
|
||
if (document.readyState !== 'loading') { | ||
setTimeout(() => { | ||
instance.init(); | ||
}, 500); | ||
return; | ||
} | ||
|
||
document.addEventListener("DOMContentLoaded", () => { | ||
setTimeout(() => { | ||
instance.init(); | ||
}, 500); | ||
}); | ||
} | ||
} | ||
|
||
RocketLcpBeacon.run(); |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
Oops, something went wrong.