Preload external css files to prevent flash of unstyled content (#1558)

* Preload external css files to prevent flash of unstyled content

This is an implementation of the approach mentioned in the commit
message of 46b85d5.

* Tweak max_age value for css files

Ensure that css preloading works even on a slow PC.
This commit is contained in:
Hikaru Y 2021-12-16 20:47:10 +09:00 committed by GitHub
parent 80ed94ed08
commit e92d7f13e3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 59 additions and 1 deletions

View file

@ -172,8 +172,13 @@ def _handle_local_file_request(request: LocalFileRequest) -> Response:
try: try:
mimetype = _mime_for_path(fullpath) mimetype = _mime_for_path(fullpath)
if os.path.exists(fullpath): if os.path.exists(fullpath):
if fullpath.endswith(".css"):
# make changes to css files immediately reflected in the webview
max_age = 10
else:
max_age = 60 * 60
return flask.send_file( return flask.send_file(
fullpath, mimetype=mimetype, conditional=True, max_age=60 * 60 # type: ignore[call-arg] fullpath, mimetype=mimetype, conditional=True, max_age=max_age # type: ignore[call-arg]
) )
else: else:
print(f"Not found: {path}") print(f"Not found: {path}")

49
ts/reviewer/css.ts Normal file
View file

@ -0,0 +1,49 @@
// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
type CssElementType = HTMLStyleElement | HTMLLinkElement;
const preloadCssClassName = "preload-css";
const template = document.createElement("template");
export async function maybePreloadExternalCss(html: string): Promise<void> {
clearPreloadedCss();
template.innerHTML = html;
const externalCssElements = extractExternalCssElements(template.content);
if (externalCssElements.length) {
await Promise.race([
Promise.all(externalCssElements.map(injectAndLoadCss)),
new Promise((r) => setTimeout(r, 500)),
]);
}
}
function clearPreloadedCss(): void {
[...document.head.getElementsByClassName(preloadCssClassName)].forEach((css) =>
css.remove(),
);
}
function extractExternalCssElements(fragment: DocumentFragment): CssElementType[] {
return <CssElementType[]>(
[...fragment.querySelectorAll("style, link")].filter(
(css) =>
(css instanceof HTMLStyleElement &&
css.innerHTML.includes("@import")) ||
(css instanceof HTMLLinkElement && css.rel === "stylesheet"),
)
);
}
function injectAndLoadCss(css: CssElementType): Promise<void> {
return new Promise((resolve) => {
css.classList.add(preloadCssClassName);
// this prevents the css from affecting the page rendering
css.media = "print";
css.addEventListener("load", () => resolve());
css.addEventListener("error", () => resolve());
document.head.appendChild(css);
});
}

View file

@ -21,6 +21,7 @@ globalThis.anki.mutateNextCardStates = mutateNextCardStates;
import { bridgeCommand } from "../lib/bridgecommand"; import { bridgeCommand } from "../lib/bridgecommand";
import { allImagesLoaded, preloadAnswerImages } from "./images"; import { allImagesLoaded, preloadAnswerImages } from "./images";
import { maybePreloadExternalCss } from "./css";
declare const MathJax: any; declare const MathJax: any;
type Callback = () => void | Promise<void>; type Callback = () => void | Promise<void>;
@ -113,6 +114,9 @@ export async function _updateQA(
const qa = document.getElementById("qa")!; const qa = document.getElementById("qa")!;
// prevent flash of unstyled content when external css used
await maybePreloadExternalCss(html);
qa.style.opacity = "0"; qa.style.opacity = "0";
try { try {