diff --git a/.cargo/config.toml b/.cargo/config.toml
index cdef3ae2573ed9bca9b2280d17e897773283c6b0..f86bcbdf913aca0dbb998d34cd0ef3706c9cef7b 100644
--- a/.cargo/config.toml
+++ b/.cargo/config.toml
@@ -5,7 +5,7 @@
 
 [target.x86_64-unknown-linux-gnu]
 linker = "clang"
-rustflags = ["-Clink-arg=-fuse-ld=lld", "-Zshare-generics=y"]
+rustflags = ["-Clink-arg=-fuse-ld=mold", "-Zshare-generics=y"]
 
 # NOTE: you must manually install https://github.com/michaeleisel/zld on mac. you can easily do this with the "brew" package manager:
 # `brew install michaeleisel/zld/zld`
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index c13b95f1d7f3f6d8943ee16ace1873e696c558c9..0867f1bd693312f180c44299939675f09317500c 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -52,30 +52,30 @@ build-linux:
   only:
     - trunk
 
-build-arm64:
-  image: "r.lcr.gr/microhacks/bevy-builder:arm64"
-  tags:
-    - arm64
-  stage: build
-  before_script:
-    - export CARGO_HOME="${CI_PROJECT_DIR}/.cargo"
-    - export PATH="${CI_PROJECT_DIR}/.cargo/bin:$PATH"
-  cache:
-    key: build-cache-arm64
-    paths:
-      - .cargo/registry/cache
-      - .cargo/registry/index
-      - .cargo/git/db
-      - .cargo/bin/
-      - target/
-  script:
-    - cargo build --release -p ${BINARY_FOLDER} --target aarch64-unknown-linux-gnu
-  artifacts:
-    expire_in: 1 day
-    paths:
-      - target/aarch64-unknown-linux-gnu/release/game_core
-  only:
-    - trunk
+#build-arm64:
+#  image: "r.lcr.gr/microhacks/bevy-builder:arm64"
+#  tags:
+#    - arm64
+#  stage: build
+#  before_script:
+#    - export CARGO_HOME="${CI_PROJECT_DIR}/.cargo"
+#    - export PATH="${CI_PROJECT_DIR}/.cargo/bin:$PATH"
+#  cache:
+#    key: build-cache-arm64
+#    paths:
+#      - .cargo/registry/cache
+#      - .cargo/registry/index
+#      - .cargo/git/db
+#      - .cargo/bin/
+#      - target/
+#  script:
+#    - cargo build --release -p ${BINARY_FOLDER} --target aarch64-unknown-linux-gnu
+#  artifacts:
+#    expire_in: 1 day
+#    paths:
+#      - target/aarch64-unknown-linux-gnu/release/game_core
+#  only:
+#    - trunk
 
 build-web:
   stage: build
@@ -112,16 +112,16 @@ package-all:
     - cp -r assets dist/assets
     - cp target/x86_64-unknown-linux-gnu/release/${BINARY_NAME} "dist/${BINARY_NAME}"
     - cp target/x86_64-pc-windows-gnu/release/${BINARY_NAME}.exe "dist/${BINARY_NAME}.exe"
-    - cp target/aarch64-unknown-linux-gnu/release/${BINARY_NAME} "dist/${BINARY_NAME}.arm64"
+#    - cp target/aarch64-unknown-linux-gnu/release/${BINARY_NAME} "dist/${BINARY_NAME}.arm64"
     - cd "${CI_PROJECT_DIR}/dist" && zip -r "windows.zip" "./${BINARY_NAME}.exe" ./assets
     - cd "${CI_PROJECT_DIR}/dist" && zip -r "linux.x86.zip" "./${BINARY_NAME}" ./assets
-    - cd "${CI_PROJECT_DIR}/dist" && zip -r "linux.arm64.zip" "./${BINARY_NAME}.arm64" ./assets
+#    - cd "${CI_PROJECT_DIR}/dist" && zip -r "linux.arm64.zip" "./${BINARY_NAME}.arm64" ./assets
     - cd "${CI_PROJECT_DIR}/${BINARY_FOLDER}/dist" && zip -r "web.zip" ./*
     - cd "${CI_PROJECT_DIR}" && mv "${CI_PROJECT_DIR}/game_core/dist/web.zip" "${CI_PROJECT_DIR}/dist/web.zip"
   dependencies:
     - build-windows
     - build-linux
-    - build-arm64
+#    - build-arm64
     - build-web
   artifacts:
     expire_in: 7 days
@@ -129,7 +129,7 @@ package-all:
       - dist/web.zip
       - dist/windows.zip
       - dist/linux.x86.zip
-      - dist/linux.arm64.zip
+#      - dist/linux.arm64.zip
   only:
     - trunk
 
diff --git a/Cargo.toml b/Cargo.toml
index a1651997580825f71a14895ee1f3d613f49b352c..976981cdc200b08933939cc652d70dcbded14310 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -6,6 +6,9 @@ members = [
     "game_core",
 ]
 
+[profile.dev.package."*"]
+opt-level = 3
+
 [workspace.dependencies]
 fastrand = "1.8.0"
 anyhow = "1.0.66"
@@ -19,7 +22,7 @@ micro_musicbox = { version = "0.5.0", features = ["mp3"] }
 micro_banimate = "0.2.1"
 
 [workspace.dependencies.bevy]
-version = "0.9.0"
+version = "0.9.1"
 default-features = false
 features = [
     "bevy_asset",
@@ -32,7 +35,3 @@ features = [
     "serialize",
     "filesystem_watcher"
 ]
-
-[profile.release]
-debug = 0
-opt-level = 3
diff --git a/Makefile b/Makefile
index 2dc4e111f5fabb10dc0a0a26fea2a3e955504ff9..34f9ed63bc73664e0d9baa27b18723e7ab099823 100644
--- a/Makefile
+++ b/Makefile
@@ -22,10 +22,10 @@ assets:
 
 run:
 	RUSTFLAGS="-Awarnings" \
-	cargo run --release --features "bevy/dynamic" -p game_core
+	cargo run -p game_core
 
 run-web:
-	cd game_core && trunk serve --release
+	cd game_core && trunk serve
 
 check:
 	cargo check --release --features "bevy/dynamic" -p game_core
diff --git a/build/web/autofocus.js b/build/web/autofocus.js
index bd59c5069600d882aa67626d3d1d4c2cb5c8b8c1..e84019d587ad1a69a18886291d8c1db531e54786 100644
--- a/build/web/autofocus.js
+++ b/build/web/autofocus.js
@@ -12,6 +12,11 @@ document.addEventListener('DOMContentLoaded', function() {
 					if (mutation.addedNodes.length > 0) {
 						for (const node of mutation.addedNodes) {
 							if (node.nodeName.toLowerCase() === 'canvas') {
+								let viewport = document.querySelector(`meta[name="viewport"]`)
+								if (viewport) {
+									viewport.content = 'height=720, width=1280, initial-scale=1, user-scalable=no'
+								}
+
 								node.focus();
 								if (window.focusHandler) {
 									window.focusHandler.disconnect()
diff --git a/build/web/intercept.js b/build/web/intercept.js
new file mode 100644
index 0000000000000000000000000000000000000000..eb2ab9ec8d9ae54f0ed64fde8c61ad0ec2c78a68
--- /dev/null
+++ b/build/web/intercept.js
@@ -0,0 +1,152 @@
+window.originalFetch = window.fetch
+window.sessionFetchCache = {}
+window.sessionProgressState = {}
+
+function fetchAndReportProgress(url) {
+	const outbound = new XMLHttpRequest()
+
+	let defer = {}
+	let promise = new Promise((resolve, reject) => {
+		defer = { resolve, reject }
+	})
+
+	outbound.onprogress = function(event) {
+		window.sessionProgressState[url] = event.lengthComputable ? {
+			indeterminate: false,
+			loaded: event.loaded,
+			total: event.total,
+			complete: event.loaded === event.total,
+			error: null,
+		} : {
+			indeterminate: true,
+			loaded: 0,
+			total: 0,
+			complete: false,
+			error: null
+		}
+	}
+	outbound.onerror = function(event) {
+		window.sessionProgressState[url].error = true
+		defer.reject(new Error("Failed to get"))
+	}
+	outbound.onloadstart = function(event) {
+		window.sessionProgressState[url] = {
+			indeterminate: !event.lengthComputable,
+			loaded: event.loaded,
+			total: event.total,
+			complete: false,
+			error: null,
+		}
+	}
+	outbound.onload = function(event) {
+		window.sessionProgressState[url] = {
+			indeterminate: !event.lengthComputable,
+			loaded: event.loaded,
+			total: event.total,
+			complete: true,
+			error: null,
+		}
+		window.sessionFetchCache[url] = new Uint8Array(outbound.response)
+		defer.resolve(window.sessionFetchCache[url])
+	}
+
+	outbound.responseType = "arraybuffer"
+	outbound.open("GET", url, true)
+
+	outbound.send()
+
+	return promise
+}
+
+function checkLoop(url) {
+	let defer = {}
+	let promise = new Promise((resolve, reject) => {
+		defer = { resolve, reject }
+	})
+
+	function inner() {
+		if (window.sessionFetchCache[url]) {
+			defer.resolve(window.sessionFetchCache[url])
+		} else if (window.sessionProgressState[url]) {
+			const val = window.sessionProgressState[url]
+			if (val.error) {
+				defer.reject(new Error("Failed to get"))
+			} else {
+				requestAnimationFrame(inner)
+			}
+		} else {
+			defer.reject(new Error("Missing entry"))
+		}
+
+	}
+
+	requestAnimationFrame(inner)
+
+	return promise
+}
+
+window.fetch = async function interceptedFetch(...params) {
+	if (params.length === 1 && typeof params[0] === "string") {
+		const path = params[0]
+		if (path.endsWith("wasm")) {
+			try {
+				const value = await checkLoop(path)
+				return new Response(value, {
+					headers: {},
+					status: 200,
+					statusText: 'ok'
+				})
+			} catch(_) {
+				return window.originalFetch(...params)
+			}
+		}
+	}
+	return window.originalFetch(...params)
+}
+
+window.interceptPreloads = function() {
+	let elems = document.querySelectorAll('link[rel="preload"][as="fetch"]')
+	const checks = []
+	elems.forEach(item => {
+		checks.push(item.href)
+		fetchAndReportProgress(item.href)
+	})
+
+	const start_container = document.getElementById('start_container')
+	for (const child of Array.from(start_container.children)) {
+		start_container.removeChild(child)
+	}
+
+	start_container.innerHTML = `<div style="width: 85%; height: 2.5rem; margin: auto; background-color: lightgray">
+	<div id="progressbar" style="width: 0%; height: 2.5rem; background-color: cornflowerblue"></div>
+</div>`
+
+	function check_progress() {
+		let total_progress = 0
+		let total_total = 0
+
+		let is_complete = checks.every(url => window.sessionFetchCache.hasOwnProperty(url))
+
+		if (is_complete) {
+			document.getElementById('progressbar').style.width = `100%`
+			start_container.innerHTML = document.getElementById('content_loaded').innerHTML
+			window.attachStartListener()
+		} else {
+			for (const url of checks) {
+				let value = window.sessionProgressState[url]
+				console.log(value)
+
+				if (value) {
+					total_progress += value.loaded
+					total_total += value.total
+				}
+			}
+
+			const percent = Math.floor((total_progress / total_total) * 100)
+			document.getElementById('progressbar').style.width = `${percent}%`
+			requestAnimationFrame(check_progress)
+		}
+	}
+
+	check_progress()
+}
diff --git a/game_core/flit.toml b/game_core/flit.toml
index 38de045c1ae3b5ab35209b797ffae883c0893c6d..5a735d6939706c0caa5f08ee8962df26b834a1e2 100644
--- a/game_core/flit.toml
+++ b/game_core/flit.toml
@@ -2,7 +2,7 @@
 action = "wrap"
 template ='''
 <script>
-document.addEventListener('DOMContentLoaded', () => {
+window.attachStartListener = function() {
   	document.getElementById('start_button').addEventListener('click', function() {
   		let element = document.createElement('{{{TAG}}}');
   		let attr_string = `{{{ATTR}}}`
@@ -12,11 +12,15 @@ document.addEventListener('DOMContentLoaded', () => {
   		});
   		element.innerHTML = `{{{CONTENT}}}`;
   		document.body.appendChild(element);
-  		const remove = ['start_container', 'init-styles']
+  		const remove = ['start_container', 'init-styles', 'content_loaded']
   		remove.forEach(function(id) {
 			let container = document.getElementById(id);
 			container.parentNode.removeChild(container);
   		});
   	});
-});
+};
+
+document.addEventListener('DOMContentLoaded', () => {
+	window.interceptPreloads()
+})
 </script>'''
\ No newline at end of file
diff --git a/game_core/index.html b/game_core/index.html
index 25f6496a796c87273c5b6173605b2fdbf59f524d..c7e0adedc0fb3bec6f08632dd84364a4cb29658a 100644
--- a/game_core/index.html
+++ b/game_core/index.html
@@ -4,10 +4,21 @@
 	<meta charset="utf-8"/>
 	<title>Bevy 2D Template</title> <!-- TODO: Set game name -->
 	<link data-trunk rel="copy-dir" href="../assets"/>
+	<meta name="viewport" content="width=device-width, initial-scale=1" />
+
 	<style>
-		html, body {
+		body,
+		html {
 			margin: 0;
 			padding: 0;
+			width: 100vw;
+			height: 100vh;
+		}
+		canvas {
+			display: block;
+		}
+		body {
+			overflow: hidden;
 		}
 	</style>
 	<style id="init-styles">
@@ -16,9 +27,8 @@
 		}
 
 		#start_container {
-			width: auto;
+			width: 100vw;
 			height: 100vh;
-			aspect-ratio: 1;
 			display: flex;
 			justify-content: center;
 			align-items: center;
@@ -47,8 +57,9 @@
 		}
 
 		#start_button {
-			width: 25%;
+			width: auto;
 			height: 25%;
+			aspect-ratio: 1;
 			background-color: black;
 			border-radius: 50%;
 			cursor: pointer;
@@ -83,26 +94,27 @@
 <body>
 
 <main id="start_container">
+</main>
+
+<div style="display: none" id="content_loaded">
 	<svg
-		width="512"
-		height="512"
-		viewBox="0 0 512 512"
-		id="start_button"
-		xmlns="http://www.w3.org/2000/svg">
+			viewBox="0 0 512 512"
+			id="start_button"
+			xmlns="http://www.w3.org/2000/svg">
 		<path
-			class="play-button-component swircle"
-			d="M 482.86983,256 A 226.86983,226.86983 0 0 1 256,482.86983 226.86983,226.86983 0 0 1 29.130173,256 226.86983,226.86983 0 0 1 256,29.130173 226.86983,226.86983 0 0 1 482.86983,256 Z"/>
+				class="play-button-component swircle"
+				d="M 482.86983,256 A 226.86983,226.86983 0 0 1 256,482.86983 226.86983,226.86983 0 0 1 29.130173,256 226.86983,226.86983 0 0 1 256,29.130173 226.86983,226.86983 0 0 1 482.86983,256 Z"/>
 		<path
-			class="play-button-component swircle"
-			d="M 447.06812,256 A 191.06812,191.06812 0 0 1 256,447.06812 191.06812,191.06812 0 0 1 64.931885,256 191.06812,191.06812 0 0 1 256,64.931885 191.06812,191.06812 0 0 1 447.06812,256 Z"/>
+				class="play-button-component swircle"
+				d="M 447.06812,256 A 191.06812,191.06812 0 0 1 256,447.06812 191.06812,191.06812 0 0 1 64.931885,256 191.06812,191.06812 0 0 1 256,64.931885 191.06812,191.06812 0 0 1 447.06812,256 Z"/>
 		<path
-			class="play-button-component"
-			d="M 194.45831,348.47991 V 163.5198 l 159.08378,91.84706 -126.14795,72.83155 V 232.4989" />
+				class="play-button-component"
+				d="M 194.45831,348.47991 V 163.5198 l 159.08378,91.84706 -126.14795,72.83155 V 232.4989" />
 	</svg>
-
 	<h1>Press Start To Play</h1>
-</main>
+</div>
 
+<link data-trunk rel="inline" href="../build/web/intercept.js"/>
 <link data-trunk rel="inline" href="../build/web/audio.js"/>
 <link data-trunk rel="inline" href="../build/web/autofocus.js"/>
 
diff --git a/game_core/src/system/mod.rs b/game_core/src/system/mod.rs
index 309ab329d7595034fdc2a5fcbbe46f67f4859192..5913484f098a9e8e3c2fd63d16c5253e0fad790f 100644
--- a/game_core/src/system/mod.rs
+++ b/game_core/src/system/mod.rs
@@ -4,3 +4,4 @@ pub mod load_config;
 pub mod resources;
 pub mod utilities;
 pub mod window;
+pub mod web;
\ No newline at end of file
diff --git a/game_core/src/system/resources.rs b/game_core/src/system/resources.rs
index fbab91f797256313c1e7b8bbd7b34c6c44854a58..f0741d0d88d4075920c7135c08cbb07e976a41b5 100644
--- a/game_core/src/system/resources.rs
+++ b/game_core/src/system/resources.rs
@@ -43,6 +43,6 @@ pub fn configure_default_plugins() -> PluginGroupBuilder {
 pub struct InitAppPlugins;
 impl PluginGroup for InitAppPlugins {
 	fn build(self) -> PluginGroupBuilder {
-		configure_default_plugins().add(DefaultResourcesPlugin)
+		configure_default_plugins().add(DefaultResourcesPlugin).add(super::web::WebPlugin)
 	}
 }
diff --git a/game_core/src/system/web.rs b/game_core/src/system/web.rs
new file mode 100644
index 0000000000000000000000000000000000000000..23a311c5a765e42f6a7b103bc128a67d9efc044f
--- /dev/null
+++ b/game_core/src/system/web.rs
@@ -0,0 +1,33 @@
+#[cfg(target_arch = "wasm32")]
+mod __plugin {
+	use bevy::app::{App, Plugin, CoreStage};
+	pub fn handle_startup_fullscreen() {
+		if micro_bevy_web_utils::bindings::is_touch_device() {
+			micro_bevy_web_utils::bindings::make_selector_fullscreen("canvas".to_string());
+			micro_bevy_web_utils::bindings::bind_selector_touch_events("canvas".to_string());
+			micro_bevy_web_utils::bindings::orientation_lock("landscape".to_string());
+		}
+	}
+
+	pub struct WebPlugin;
+	impl Plugin for WebPlugin {
+		fn build(&self, app: &mut App) {
+				app.add_startup_system(inner::handle_startup_fullscreen)
+					.add_system_to_stage(
+						CoreStage::First,
+						micro_bevy_web_utils::bevy::emit_touch_events,
+					);
+			}
+	}
+}
+
+#[cfg(not(target_arch = "wasm32"))]
+mod __plugin {
+	use bevy::app::{App, Plugin};
+	pub struct WebPlugin;
+	impl Plugin for WebPlugin {
+		fn build(&self, app: &mut App) {}
+	}
+}
+
+pub use __plugin::WebPlugin;
\ No newline at end of file