PERF: Refactor and optimize splash screen implementation
- Remove JS - Remove "Loading..." text. This has been been broken for a while due to some conflicting discourse-ai CSS. Also, animating the `content:` property like this requires the browser to repaint/reflow, which cannot be done while JS is executing. - Replace animated SVG with divs animated via CSS. When JS is executing, browsers pause animations of transform properties inside SVGs. This limitation does not exist on regular CSS animations. So with this change, the animation continues smoothly even you run an infinite loop in JS. To ensure the splash screen remains "contentful" for LCP purposes, an SVG background-image is used There is no change to the visual look of the animation
This commit is contained in:
parent
dc3379430d
commit
b189aed2d2
|
@ -56,8 +56,7 @@ class Discourse extends Application {
|
|||
|
||||
ready() {
|
||||
performance.mark("discourse-ready");
|
||||
const event = new CustomEvent("discourse-ready");
|
||||
document.dispatchEvent(event);
|
||||
document.querySelector("#d-splash")?.remove();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,66 +0,0 @@
|
|||
// This script is inlined in `_discourse_splash.html.erb
|
||||
const DELAY_TARGET = 2000;
|
||||
const POLLING_INTERVAL = 50;
|
||||
|
||||
const splashSvgTemplate = document.querySelector(".splash-svg-template");
|
||||
const splashTemplateClone = splashSvgTemplate.content.cloneNode(true);
|
||||
const svgElement = splashTemplateClone.querySelector("svg");
|
||||
|
||||
const svgString = new XMLSerializer().serializeToString(svgElement);
|
||||
const encodedSvg = btoa(svgString);
|
||||
|
||||
const splashWrapper = document.querySelector("#d-splash");
|
||||
const splashImage =
|
||||
splashWrapper && splashWrapper.querySelector(".preloader-image");
|
||||
|
||||
if (splashImage) {
|
||||
splashImage.src = `data:image/svg+xml;base64,${encodedSvg}`;
|
||||
|
||||
const connectStart = performance.timing.connectStart || 0;
|
||||
const targetTime = connectStart + DELAY_TARGET;
|
||||
|
||||
let splashInterval;
|
||||
let discourseReady;
|
||||
|
||||
const swapSplash = () => {
|
||||
splashWrapper &&
|
||||
splashWrapper.style.setProperty("--animation-state", "running");
|
||||
svgElement && svgElement.style.setProperty("--animation-state", "running");
|
||||
|
||||
const newSvgString = new XMLSerializer().serializeToString(svgElement);
|
||||
const newEncodedSvg = btoa(newSvgString);
|
||||
|
||||
splashImage.src = `data:image/svg+xml;base64,${newEncodedSvg}`;
|
||||
|
||||
performance.mark("discourse-splash-visible");
|
||||
|
||||
clearSplashInterval();
|
||||
};
|
||||
|
||||
const clearSplashInterval = () => {
|
||||
clearInterval(splashInterval);
|
||||
splashInterval = null;
|
||||
};
|
||||
|
||||
(() => {
|
||||
splashInterval = setInterval(() => {
|
||||
if (discourseReady) {
|
||||
clearSplashInterval();
|
||||
}
|
||||
|
||||
if (Date.now() > targetTime) {
|
||||
swapSplash();
|
||||
}
|
||||
}, POLLING_INTERVAL);
|
||||
})();
|
||||
|
||||
document.addEventListener(
|
||||
"discourse-ready",
|
||||
() => {
|
||||
discourseReady = true;
|
||||
splashWrapper && splashWrapper.remove();
|
||||
performance.mark("discourse-splash-removed");
|
||||
},
|
||||
{ once: true }
|
||||
);
|
||||
}
|
|
@ -1,23 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module SplashScreenHelper
|
||||
def self.raw_js
|
||||
if Rails.env.development?
|
||||
load_js
|
||||
else
|
||||
@loaded_js ||= load_js
|
||||
end.html_safe
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def self.load_js
|
||||
File.read("#{Rails.root}/app/assets/javascripts/discourse/dist/assets/splash-screen.js").sub(
|
||||
"//# sourceMappingURL=splash-screen.map",
|
||||
"",
|
||||
)
|
||||
rescue Errno::ENOENT
|
||||
Rails.logger.error("Unable to load splash screen JS") if Rails.env.production?
|
||||
"console.log('Unable to load splash screen JS')"
|
||||
end
|
||||
end
|
|
@ -1,114 +1,19 @@
|
|||
<%- unless customization_disabled? %>
|
||||
<section id="d-splash">
|
||||
<template class="splash-svg-template">
|
||||
<%
|
||||
empty_svg = <<~HTML
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
version="1.1"
|
||||
>
|
||||
<style>
|
||||
:root {
|
||||
--animation-state: paused;
|
||||
}
|
||||
|
||||
/* user picked a theme where the "regular" scheme is dark */
|
||||
<%- if dark_color_scheme? %>
|
||||
:root {
|
||||
--primary: #<%= ColorScheme.hex_for_name("primary", scheme_id) %>;
|
||||
--secondary: #<%= ColorScheme.hex_for_name("secondary", scheme_id) %>;
|
||||
--tertiary: #<%= ColorScheme.hex_for_name("tertiary", scheme_id) %>;
|
||||
--highlight: #<%= ColorScheme.hex_for_name("highlight", scheme_id) %>;
|
||||
--success: #<%= ColorScheme.hex_for_name("success", scheme_id) %>;
|
||||
}
|
||||
<%- else %>
|
||||
/* user picked a theme a light scheme and also enabled a dark scheme */
|
||||
|
||||
/* deal with light scheme first */
|
||||
@media (prefers-color-scheme: light) {
|
||||
:root {
|
||||
--primary: #<%= ColorScheme.hex_for_name("primary", scheme_id) %>;
|
||||
--secondary: #<%= ColorScheme.hex_for_name("secondary", scheme_id) %>;
|
||||
--tertiary: #<%= ColorScheme.hex_for_name("tertiary", scheme_id) %>;
|
||||
--highlight: #<%= ColorScheme.hex_for_name("highlight", scheme_id) %>;
|
||||
--success: #<%= ColorScheme.hex_for_name("success", scheme_id) %>;
|
||||
}
|
||||
}
|
||||
|
||||
/* then deal with dark scheme */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--primary: #<%= ColorScheme.hex_for_name("primary", dark_scheme_id) %>;
|
||||
--secondary: #<%= ColorScheme.hex_for_name("secondary", dark_scheme_id) %>;
|
||||
--tertiary: #<%= ColorScheme.hex_for_name("tertiary", dark_scheme_id) %>;
|
||||
--highlight: #<%= ColorScheme.hex_for_name("highlight", dark_scheme_id) %>;
|
||||
--success: #<%= ColorScheme.hex_for_name("success", dark_scheme_id) %>;
|
||||
}
|
||||
}
|
||||
<%- end %>
|
||||
|
||||
/* these styles need to live here because the SVG has a different scope */
|
||||
.dots {
|
||||
animation-name: loader;
|
||||
animation-timing-function: ease-in-out;
|
||||
animation-duration: 3s;
|
||||
animation-iteration-count: infinite;
|
||||
animation-play-state: var(--animation-state);
|
||||
stroke: #fff;
|
||||
stroke-width: 0.5px;
|
||||
transform-origin: center;
|
||||
opacity: 0;
|
||||
r: max(1vw, 11px);
|
||||
cy: 50%;
|
||||
filter: saturate(2) opacity(0.85);
|
||||
fill: var(--tertiary);
|
||||
}
|
||||
|
||||
.dots:nth-child(2) {
|
||||
animation-delay: 0.15s;
|
||||
}
|
||||
|
||||
.dots:nth-child(3) {
|
||||
animation-delay: 0.3s;
|
||||
}
|
||||
|
||||
.dots:nth-child(4) {
|
||||
animation-delay: 0.45s;
|
||||
}
|
||||
|
||||
.dots:nth-child(5) {
|
||||
animation-delay: 0.6s;
|
||||
}
|
||||
|
||||
@keyframes loader {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: scale(1);
|
||||
}
|
||||
45% {
|
||||
opacity: 1;
|
||||
transform: scale(0.7);
|
||||
}
|
||||
65% {
|
||||
opacity: 1;
|
||||
transform: scale(0.7);
|
||||
}
|
||||
100% {
|
||||
opacity: 0;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<g class="container">
|
||||
<circle class="dots" cx="30vw" />
|
||||
<circle class="dots" cx="40vw" />
|
||||
<circle class="dots" cx="50vw" />
|
||||
<circle class="dots" cx="60vw" />
|
||||
<circle class="dots" cx="70vw" />
|
||||
</g>
|
||||
<!--
|
||||
LCP candidate image
|
||||
#{"." * 5000}
|
||||
-->
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
HTML
|
||||
%>
|
||||
<%- unless customization_disabled? %>
|
||||
<section id="d-splash">
|
||||
<style>
|
||||
html {
|
||||
overflow-y: hidden !important;
|
||||
|
@ -116,81 +21,59 @@
|
|||
|
||||
/* user picked a theme where the "regular" scheme is dark */
|
||||
<%- if dark_color_scheme? %>
|
||||
html {
|
||||
background-color: #<%= ColorScheme.hex_for_name("secondary", scheme_id) %>;
|
||||
}
|
||||
|
||||
#d-splash .preloader-text-wrapper {
|
||||
color: #<%= ColorScheme.hex_for_name("primary", scheme_id) %>;
|
||||
}
|
||||
<%- else %>
|
||||
/* user picked a theme a light scheme and also enabled a dark scheme */
|
||||
|
||||
/* deal with light scheme first */
|
||||
@media (prefers-color-scheme: light) {
|
||||
html {
|
||||
background-color: #<%= ColorScheme.hex_for_name("secondary", scheme_id) %>;
|
||||
}
|
||||
|
||||
#d-splash .preloader-text-wrapper {
|
||||
color: #<%= ColorScheme.hex_for_name("primary", scheme_id) %>;
|
||||
#d-splash {
|
||||
--dot-color: #<%= ColorScheme.hex_for_name("tertiary", scheme_id) %>;
|
||||
}
|
||||
}
|
||||
<%- else %>
|
||||
/* user picked a theme a light scheme and also enabled a dark scheme */
|
||||
|
||||
/* then deal with dark scheme */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
html {
|
||||
background-color: #<%= ColorScheme.hex_for_name("secondary", dark_scheme_id) %>;
|
||||
/* deal with light scheme first */
|
||||
@media (prefers-color-scheme: light) {
|
||||
html {
|
||||
background-color: #<%= ColorScheme.hex_for_name("secondary", scheme_id) %>;
|
||||
}
|
||||
|
||||
#d-splash {
|
||||
--dot-color: #<%= ColorScheme.hex_for_name("tertiary", scheme_id) %>;
|
||||
}
|
||||
}
|
||||
|
||||
#d-splash .preloader-text-wrapper {
|
||||
color: #<%= ColorScheme.hex_for_name("primary", dark_scheme_id) %>;
|
||||
/* then deal with dark scheme */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
html {
|
||||
background-color: #<%= ColorScheme.hex_for_name("secondary", dark_scheme_id) %>;
|
||||
}
|
||||
|
||||
#d-splash {
|
||||
--dot-color: #<%= ColorScheme.hex_for_name("tertiary", dark_scheme_id) %>;
|
||||
}
|
||||
}
|
||||
}
|
||||
<%- end %>
|
||||
|
||||
#d-splash {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
backface-visibility: hidden;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
width: 100vw;
|
||||
z-index: 1001;
|
||||
--animation-state: paused;
|
||||
}
|
||||
|
||||
#d-splash .preloader-image {
|
||||
max-width: 100%;
|
||||
--splash-dot-size: max(1vw, 25px);
|
||||
--splash-dot-spacing: calc(var(--splash-dot-size) * 1.5);
|
||||
width: calc((var(--splash-dot-size) + var(--splash-dot-spacing)) * 5);
|
||||
height: 100vh;
|
||||
background-image: url('data:image/svg+xml;base64,<%= Base64.strict_encode64 empty_svg %>');
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
#d-splash .preloader-text-wrapper {
|
||||
font-family: sans-serif;
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
animation: fade-in 0.5s ease-in-out;
|
||||
animation-delay: 1s;
|
||||
animation-fill-mode: forwards;
|
||||
animation-play-state: var(--animation-state);
|
||||
margin-bottom: -4em;
|
||||
}
|
||||
|
||||
#d-splash .preloader-text:after {
|
||||
animation: loading-text 3s infinite;
|
||||
content: "";
|
||||
position: absolute;
|
||||
margin: 0 0.1em;
|
||||
left: 100%;
|
||||
}
|
||||
|
||||
.rtl #d-splash .preloader-text:after {
|
||||
left: 0;
|
||||
right: 100%;
|
||||
}
|
||||
|
||||
@keyframes fade-in {
|
||||
@keyframes d-splash-fade-in {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
|
@ -199,30 +82,55 @@
|
|||
}
|
||||
}
|
||||
|
||||
@keyframes loading-text {
|
||||
.dots {
|
||||
animation-name: d-splash-loader;
|
||||
animation-timing-function: ease-in-out;
|
||||
animation-duration: 3s;
|
||||
animation-iteration-count: infinite;
|
||||
animation-delay: calc(var(--n) * 0.15s);
|
||||
|
||||
position: absolute;
|
||||
top: calc(50% - var(--splash-dot-size) / 2);
|
||||
left: calc((50% - var(--splash-dot-size) / 2) + (var(--n) * var(--splash-dot-spacing)));
|
||||
transform-origin: calc((var(--splash-dot-spacing) * var(--n) * -1) + var(--splash-dot-size)/2) center;
|
||||
|
||||
width: var(--splash-dot-size);
|
||||
height: var(--splash-dot-size);
|
||||
border-radius: 50%;
|
||||
|
||||
border: 0.5px solid #fff;
|
||||
background-color: var(--dot-color);
|
||||
filter: saturate(2) opacity(0.85);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
@keyframes d-splash-loader {
|
||||
0% {
|
||||
content: "";
|
||||
opacity: 0;
|
||||
transform: scale(1);
|
||||
}
|
||||
25% {
|
||||
content: ".";
|
||||
45% {
|
||||
opacity: 1;
|
||||
transform: scale(0.7);
|
||||
}
|
||||
50% {
|
||||
content: "..";
|
||||
65% {
|
||||
opacity: 1;
|
||||
transform: scale(0.7);
|
||||
}
|
||||
75% {
|
||||
content: "...";
|
||||
100% {
|
||||
opacity: 0;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
<img
|
||||
class="preloader-image"
|
||||
src="data:image/svg;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"
|
||||
alt="<%=SiteSetting.title%>"
|
||||
/>
|
||||
|
||||
<div class="preloader-text-wrapper">
|
||||
<div class="preloader-text"><%= I18n.t("js.preloader_text") %></div>
|
||||
<div class="preloader-image" elementtiming="discourse-splash-visible">
|
||||
<div class="dots" style="--n:-2;"></div>
|
||||
<div class="dots" style="--n:-1;"></div>
|
||||
<div class="dots" style="--n:0;"></div>
|
||||
<div class="dots" style="--n:1;"></div>
|
||||
<div class="dots" style="--n:2;"></div>
|
||||
</div>
|
||||
|
||||
<noscript>
|
||||
|
@ -236,9 +144,5 @@
|
|||
}
|
||||
</style>
|
||||
</noscript>
|
||||
|
||||
<script nonce="<%= csp_nonce_placeholder %>">
|
||||
<%= SplashScreenHelper.raw_js %>
|
||||
</script>
|
||||
</section>
|
||||
<%- end %>
|
||||
|
|
Loading…
Reference in New Issue