diff --git a/templates/base/head.tmpl b/templates/base/head.tmpl
index 70a88ff75569a..b0326e09828e2 100644
--- a/templates/base/head.tmpl
+++ b/templates/base/head.tmpl
@@ -23,6 +23,7 @@
 	<link rel="alternate icon" href="{{AssetUrlPrefix}}/img/favicon.png" type="image/png">
 	<link rel="stylesheet" href="{{AssetUrlPrefix}}/css/index.css?v={{AssetVersion}}">
 	{{template "base/head_script" .}}
+	<script src="{{AssetUrlPrefix}}/js/init.js?v={{AssetVersion}}"></script>
 	<noscript>
 		<style>
 			.dropdown:hover > .menu { display: block; }
diff --git a/templates/repo/clone_script.tmpl b/templates/repo/clone_script.tmpl
deleted file mode 100644
index afd90040fb4e7..0000000000000
--- a/templates/repo/clone_script.tmpl
+++ /dev/null
@@ -1,28 +0,0 @@
-<script>
-	// synchronously set clone button states and urls here to avoid flickering
-	// on page load. initRepoCloneLink calls this when proto changes.
-	// this applies the protocol-dependant clone url to all elements with the
-	// `js-clone-url` and `js-clone-url-vsc` classes.
-	// TODO: This localStorage setting should be moved to backend user config
-	// so it's available during rendering, then this inline script can be removed.
-	(window.updateCloneStates = function() {
-		const httpsBtn = document.getElementById('repo-clone-https');
-		const sshBtn = document.getElementById('repo-clone-ssh');
-		const value = localStorage.getItem('repo-clone-protocol') || 'https';
-		const isSSH = value === 'ssh' && sshBtn || value !== 'ssh' && !httpsBtn;
-
-		if (httpsBtn) httpsBtn.classList[!isSSH ? 'add' : 'remove']('primary');
-		if (sshBtn) sshBtn.classList[isSSH ? 'add' : 'remove']('primary');
-
-		const btn = isSSH ? sshBtn : httpsBtn;
-		if (!btn) return;
-
-		const link = btn.getAttribute('data-link');
-		for (const el of document.getElementsByClassName('js-clone-url')) {
-			el[el.nodeName === 'INPUT' ? 'value' : 'textContent'] = link;
-		}
-		for (const el of document.getElementsByClassName('js-clone-url-vsc')) {
-			el['href'] = 'vscode://vscode.git/clone?url=' + encodeURIComponent(link);
-		}
-	})();
-</script>
diff --git a/templates/repo/empty.tmpl b/templates/repo/empty.tmpl
index 24547758a7f4b..3e52fbac189d6 100644
--- a/templates/repo/empty.tmpl
+++ b/templates/repo/empty.tmpl
@@ -50,7 +50,6 @@ git push -u origin {{.Repository.DefaultBranch}}</code></pre>
 git push -u origin {{.Repository.DefaultBranch}}</code></pre>
 								</div>
 							</div>
-							{{template "repo/clone_script" .}}
 						{{end}}
 					{{else}}
 						<div class="ui segment center">
diff --git a/templates/repo/home.tmpl b/templates/repo/home.tmpl
index e1aa1c4f3b45b..c636524aa3953 100644
--- a/templates/repo/home.tmpl
+++ b/templates/repo/home.tmpl
@@ -128,7 +128,6 @@
 								<a class="item js-clone-url-vsc" href="vscode://vscode.git/clone?url={{.CloneButtonOriginLink.HTTPS}}">{{svg "gitea-vscode" 16 "mr-3"}}{{.locale.Tr "repo.clone_in_vsc"}}</a>
 							</div>
 						</button>
-						{{template "repo/clone_script" .}}{{/* the script will update `.js-clone-url` and related elements */}}
 					</div>
 				{{end}}
 				{{if and (ne $n 0) (not .IsViewFile) (not .IsBlame)}}
diff --git a/templates/repo/wiki/revision.tmpl b/templates/repo/wiki/revision.tmpl
index b49441f4eb844..da2395c2dd549 100644
--- a/templates/repo/wiki/revision.tmpl
+++ b/templates/repo/wiki/revision.tmpl
@@ -7,7 +7,6 @@
 			<div class="ui eight wide column text right df ac je">
 				<div class="ui action small input" id="clone-panel">
 					{{template "repo/clone_buttons" .}}
-					{{template "repo/clone_script" .}}
 				</div>
 			</div>
 			<div class="ui header eight wide column">
diff --git a/templates/repo/wiki/view.tmpl b/templates/repo/wiki/view.tmpl
index 3cd6617dd9ad1..6d991a8ea9aa5 100644
--- a/templates/repo/wiki/view.tmpl
+++ b/templates/repo/wiki/view.tmpl
@@ -31,7 +31,6 @@
 			<div class="df ac">
 				<div class="ui action small input" id="clone-panel">
 					{{template "repo/clone_buttons" .}}
-					{{template "repo/clone_script" .}}
 				</div>
 			</div>
 		</div>
diff --git a/web_src/js/init.js b/web_src/js/init.js
new file mode 100644
index 0000000000000..79fc4bfaae02d
--- /dev/null
+++ b/web_src/js/init.js
@@ -0,0 +1,39 @@
+// This script is for critical JS that needs to perform DOM mutations
+// before the initial browser paint.
+
+let attempts = 0;
+window.requestAnimationFrame(function wait() {
+  if (document.querySelector('script[src*="index.js"]') || ++attempts > 100) return init();
+  window.requestAnimationFrame(wait);
+});
+
+// This function runs before DOMContentLoaded and checks if most of the page
+// has loaded so we can do DOM mutations before anything is painted on the screen.
+function init() {
+  // Synchronously set clone button states and urls here to avoid flickering
+  // on page load. initRepoCloneLink calls this when proto changes.
+  // this applies the protocol-dependant clone url to all elements with the
+  // `js-clone-url` and `js-clone-url-vsc` classes.
+  // TODO: This localStorage setting should be moved to backend user config.
+  (window.updateCloneStates = function() {
+    const httpsBtn = document.getElementById('repo-clone-https');
+    if (!httpsBtn) return;
+    const sshBtn = document.getElementById('repo-clone-ssh');
+    const value = localStorage.getItem('repo-clone-protocol') || 'https';
+    const isSSH = value === 'ssh' && sshBtn || value !== 'ssh' && !httpsBtn;
+
+    if (httpsBtn) httpsBtn.classList[!isSSH ? 'add' : 'remove']('primary');
+    if (sshBtn) sshBtn.classList[isSSH ? 'add' : 'remove']('primary');
+
+    const btn = isSSH ? sshBtn : httpsBtn;
+    if (!btn) return;
+
+    const link = btn.getAttribute('data-link');
+    for (const el of document.getElementsByClassName('js-clone-url')) {
+      el[el.nodeName === 'INPUT' ? 'value' : 'textContent'] = link;
+    }
+    for (const el of document.getElementsByClassName('js-clone-url-vsc')) {
+      el.href = `vscode://vscode.git/clone?url=${encodeURIComponent(link)}`;
+    }
+  })();
+}
diff --git a/webpack.config.js b/webpack.config.js
index 26632429929b9..3466b21d43af3 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -50,6 +50,9 @@ const filterCssImport = (url, ...args) => {
 export default {
   mode: isProduction ? 'production' : 'development',
   entry: {
+    init: [
+      fileURLToPath(new URL('web_src/js/init.js', import.meta.url)),
+    ],
     index: [
       fileURLToPath(new URL('web_src/js/jquery.js', import.meta.url)),
       fileURLToPath(new URL('web_src/fomantic/build/semantic.js', import.meta.url)),