diff --git a/src/index.tsx b/src/index.tsx
index 4c13f0d27..d64b190a6 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -13,6 +13,7 @@ import {
} from './config';
import { enableStrict, requestMutation } from './lib/fasterdom/fasterdom';
import { selectTabState } from './global/selectors';
+import { betterView } from './util/betterView';
import { establishMultitabRole, subscribeToMasterChange } from './util/establishMultitabRole';
import { requestGlobal, subscribeToMultitabBroadcastChannel } from './util/multitab';
import { checkAndAssignPermanentWebVersion } from './util/permanentWebVersion';
@@ -81,6 +82,8 @@ async function init() {
,
document.getElementById('root')!,
);
+
+ betterView();
});
if (DEBUG) {
diff --git a/src/util/betterView.ts b/src/util/betterView.ts
new file mode 100644
index 000000000..cbde96f34
--- /dev/null
+++ b/src/util/betterView.ts
@@ -0,0 +1,92 @@
+import { animate } from './animation';
+import { fastRaf } from './schedulers';
+import { IS_IOS } from './windowEnvironment';
+
+const TEST_INTERVAL = 5000; // 5 sec
+const FRAMES_TO_TEST = 10;
+const REDUCED_FPS = 35;
+
+let isImproved = false;
+
+export function betterView() {
+ if (!IS_IOS) return;
+
+ let interval: number | undefined;
+ let lastFocusAt = Date.now();
+
+ function setupInterval() {
+ if (interval || isImproved) return;
+
+ interval = window.setInterval(testAndImprove, TEST_INTERVAL);
+ }
+
+ window.addEventListener('focus', () => {
+ const now = Date.now();
+ if (now - lastFocusAt < 100) return; // iOS triggers two `focus` events for some reason
+ lastFocusAt = now;
+
+ setupInterval();
+ testAndImprove();
+ });
+
+ window.addEventListener('blur', () => {
+ clearInterval(interval);
+ interval = undefined;
+ });
+
+ if (document.hasFocus()) {
+ setupInterval();
+ testAndImprove();
+ }
+}
+
+async function testAndImprove() {
+ const fps = await testFps();
+ if (fps <= REDUCED_FPS) {
+ improveView();
+ }
+}
+
+function testFps() {
+ return new Promise((resolve) => {
+ const frames: number[] = [];
+ let lastFrameAt = performance.now();
+
+ animate(() => {
+ const now = performance.now();
+ frames.push(now - lastFrameAt);
+ lastFrameAt = now;
+
+ if (frames.length === FRAMES_TO_TEST) {
+ const mean = frames.sort()[Math.floor(frames.length / 2)];
+ resolve(Math.round(1000 / mean));
+ return false;
+ }
+
+ return true;
+ }, fastRaf);
+ });
+}
+
+function improveView() {
+ isImproved = true;
+
+ const containerEl = document.createElement('div');
+ containerEl.style.cssText = 'position: absolute; top: 0; left: 0; width: 0; height: 100%; overflow: hidden;';
+
+ const boosterEl = document.createElement('div');
+ const height = window.screen.height * 1.5;
+ boosterEl.style.cssText = `width: 0; height: ${height}px; transform: translateX(100%); transition: transform 100ms;`;
+ boosterEl.innerHTML = ' ';
+
+ containerEl.appendChild(boosterEl);
+ document.body.appendChild(containerEl);
+
+ requestAnimationFrame(() => {
+ boosterEl.addEventListener('transitionend', () => {
+ containerEl.remove();
+ });
+
+ boosterEl.style.transform = '';
+ });
+}