diff --git a/packages/backend/src/config.ts b/packages/backend/src/config.ts
index c6e107538952df6e2b15b9ae32a7bc717f6899a2..744f9995964e1b96006bf8ba86bc1bc7390127c4 100644
--- a/packages/backend/src/config.ts
+++ b/packages/backend/src/config.ts
@@ -144,7 +144,7 @@ export function loadConfig() {
 	const clientManifestExists = fs.existsSync(_dirname + '/../../../built/_vite_/manifest.json');
 	const clientManifest = clientManifestExists ?
 		JSON.parse(fs.readFileSync(`${_dirname}/../../../built/_vite_/manifest.json`, 'utf-8'))
-		: { 'src/init.ts': { file: 'src/init.ts' } };
+		: { 'src/_boot_.ts': { file: 'src/_boot_.ts' } };
 	const config = yaml.load(fs.readFileSync(path, 'utf-8')) as Source;
 
 	const mixin = {} as Mixin;
@@ -165,7 +165,7 @@ export function loadConfig() {
 	mixin.authUrl = `${mixin.scheme}://${mixin.host}/auth`;
 	mixin.driveUrl = `${mixin.scheme}://${mixin.host}/files`;
 	mixin.userAgent = `Misskey/${meta.version} (${config.url})`;
-	mixin.clientEntry = clientManifest['src/init.ts'];
+	mixin.clientEntry = clientManifest['src/_boot_.ts'];
 	mixin.clientManifestExists = clientManifestExists;
 
 	const externalMediaProxy = config.mediaProxy ?
diff --git a/packages/frontend/src/_boot_.ts b/packages/frontend/src/_boot_.ts
new file mode 100644
index 0000000000000000000000000000000000000000..a8efafca61d9a9395c655059c003b9cb03131bd5
--- /dev/null
+++ b/packages/frontend/src/_boot_.ts
@@ -0,0 +1,12 @@
+// https://vitejs.dev/config/build-options.html#build-modulepreload
+import 'vite/modulepreload-polyfill';
+
+import '@/style.scss';
+import { mainBoot } from './boot/main-boot';
+import { subBoot } from './boot/sub-boot';
+
+if (['/share', '/auth', '/miauth'].includes(location.pathname)) {
+	subBoot();
+} else {
+	mainBoot();
+}
diff --git a/packages/frontend/src/boot/common.ts b/packages/frontend/src/boot/common.ts
new file mode 100644
index 0000000000000000000000000000000000000000..e9d6586f85778991740a248580448fc53db39158
--- /dev/null
+++ b/packages/frontend/src/boot/common.ts
@@ -0,0 +1,263 @@
+import { computed, createApp, watch, markRaw, version as vueVersion, defineAsyncComponent, App } from 'vue';
+import { compareVersions } from 'compare-versions';
+import JSON5 from 'json5';
+import widgets from '@/widgets';
+import directives from '@/directives';
+import components from '@/components';
+import { version, ui, lang, updateLocale } from '@/config';
+import { applyTheme } from '@/scripts/theme';
+import { isDeviceDarkmode } from '@/scripts/is-device-darkmode';
+import { i18n, updateI18n } from '@/i18n';
+import { confirm, alert, post, popup, toast } from '@/os';
+import { $i, refreshAccount, login, updateAccount, signout } from '@/account';
+import { defaultStore, ColdDeviceStorage } from '@/store';
+import { fetchInstance, instance } from '@/instance';
+import { deviceKind } from '@/scripts/device-kind';
+import { reloadChannel } from '@/scripts/unison-reload';
+import { reactionPicker } from '@/scripts/reaction-picker';
+import { getUrlWithoutLoginId } from '@/scripts/login-id';
+import { getAccountFromId } from '@/scripts/get-account-from-id';
+import { deckStore } from '@/ui/deck/deck-store';
+import { miLocalStorage } from '@/local-storage';
+import { fetchCustomEmojis } from '@/custom-emojis';
+import { mainRouter } from '@/router';
+
+export async function common(createVue: () => App<Element>) {
+	console.info(`Misskey v${version}`);
+
+	if (_DEV_) {
+		console.warn('Development mode!!!');
+
+		console.info(`vue ${vueVersion}`);
+
+		// eslint-disable-next-line @typescript-eslint/no-explicit-any
+		(window as any).$i = $i;
+		// eslint-disable-next-line @typescript-eslint/no-explicit-any
+		(window as any).$store = defaultStore;
+
+		window.addEventListener('error', event => {
+			console.error(event);
+			/*
+			alert({
+				type: 'error',
+				title: 'DEV: Unhandled error',
+				text: event.message
+			});
+			*/
+		});
+
+		window.addEventListener('unhandledrejection', event => {
+			console.error(event);
+			/*
+			alert({
+				type: 'error',
+				title: 'DEV: Unhandled promise rejection',
+				text: event.reason
+			});
+			*/
+		});
+	}
+
+	const splash = document.getElementById('splash');
+	// 念のためnullチェック(HTMLが古い場合があるため(そのうち消す))
+	if (splash) splash.addEventListener('transitionend', () => {
+		splash.remove();
+	});
+
+	let isClientUpdated = false;
+
+	//#region クライアントが更新されたかチェック
+	const lastVersion = miLocalStorage.getItem('lastVersion');
+	if (lastVersion !== version) {
+		miLocalStorage.setItem('lastVersion', version);
+
+		// テーマリビルドするため
+		miLocalStorage.removeItem('theme');
+
+		try { // 変なバージョン文字列来るとcompareVersionsでエラーになるため
+			if (lastVersion != null && compareVersions(version, lastVersion) === 1) {
+				isClientUpdated = true;
+			}
+		} catch (err) { /* empty */ }
+	}
+	//#endregion
+
+	//#region Detect language & fetch translations
+	const localeVersion = miLocalStorage.getItem('localeVersion');
+	const localeOutdated = (localeVersion == null || localeVersion !== version);
+	if (localeOutdated) {
+		const res = await window.fetch(`/assets/locales/${lang}.${version}.json`);
+		if (res.status === 200) {
+			const newLocale = await res.text();
+			const parsedNewLocale = JSON.parse(newLocale);
+			miLocalStorage.setItem('locale', newLocale);
+			miLocalStorage.setItem('localeVersion', version);
+			updateLocale(parsedNewLocale);
+			updateI18n(parsedNewLocale);
+		}
+	}
+	//#endregion
+
+	// タッチデバイスでCSSの:hoverを機能させる
+	document.addEventListener('touchend', () => {}, { passive: true });
+
+	// 一斉リロード
+	reloadChannel.addEventListener('message', path => {
+		if (path !== null) location.href = path;
+		else location.reload();
+	});
+
+	// If mobile, insert the viewport meta tag
+	if (['smartphone', 'tablet'].includes(deviceKind)) {
+		const viewport = document.getElementsByName('viewport').item(0);
+		viewport.setAttribute('content',
+			`${viewport.getAttribute('content')}, minimum-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover`);
+	}
+
+	//#region Set lang attr
+	const html = document.documentElement;
+	html.setAttribute('lang', lang);
+	//#endregion
+
+	await defaultStore.ready;
+	await deckStore.ready;
+
+	const fetchInstanceMetaPromise = fetchInstance();
+
+	fetchInstanceMetaPromise.then(() => {
+		miLocalStorage.setItem('v', instance.version);
+	});
+
+	//#region loginId
+	const params = new URLSearchParams(location.search);
+	const loginId = params.get('loginId');
+
+	if (loginId) {
+		const target = getUrlWithoutLoginId(location.href);
+
+		if (!$i || $i.id !== loginId) {
+			const account = await getAccountFromId(loginId);
+			if (account) {
+				await login(account.token, target);
+			}
+		}
+
+		history.replaceState({ misskey: 'loginId' }, '', target);
+	}
+	//#endregion
+
+	// NOTE: この処理は必ずクライアント更新チェック処理より後に来ること(テーマ再構築のため)
+	watch(defaultStore.reactiveState.darkMode, (darkMode) => {
+		applyTheme(darkMode ? ColdDeviceStorage.get('darkTheme') : ColdDeviceStorage.get('lightTheme'));
+	}, { immediate: miLocalStorage.getItem('theme') == null });
+
+	const darkTheme = computed(ColdDeviceStorage.makeGetterSetter('darkTheme'));
+	const lightTheme = computed(ColdDeviceStorage.makeGetterSetter('lightTheme'));
+
+	watch(darkTheme, (theme) => {
+		if (defaultStore.state.darkMode) {
+			applyTheme(theme);
+		}
+	});
+
+	watch(lightTheme, (theme) => {
+		if (!defaultStore.state.darkMode) {
+			applyTheme(theme);
+		}
+	});
+
+	//#region Sync dark mode
+	if (ColdDeviceStorage.get('syncDeviceDarkMode')) {
+		defaultStore.set('darkMode', isDeviceDarkmode());
+	}
+
+	window.matchMedia('(prefers-color-scheme: dark)').addListener(mql => {
+		if (ColdDeviceStorage.get('syncDeviceDarkMode')) {
+			defaultStore.set('darkMode', mql.matches);
+		}
+	});
+	//#endregion
+
+	fetchInstanceMetaPromise.then(() => {
+		if (defaultStore.state.themeInitial) {
+			if (instance.defaultLightTheme != null) ColdDeviceStorage.set('lightTheme', JSON5.parse(instance.defaultLightTheme));
+			if (instance.defaultDarkTheme != null) ColdDeviceStorage.set('darkTheme', JSON5.parse(instance.defaultDarkTheme));
+			defaultStore.set('themeInitial', false);
+		}
+	});
+
+	watch(defaultStore.reactiveState.useBlurEffectForModal, v => {
+		document.documentElement.style.setProperty('--modalBgFilter', v ? 'blur(4px)' : 'none');
+	}, { immediate: true });
+
+	watch(defaultStore.reactiveState.useBlurEffect, v => {
+		if (v) {
+			document.documentElement.style.removeProperty('--blur');
+		} else {
+			document.documentElement.style.setProperty('--blur', 'none');
+		}
+	}, { immediate: true });
+
+	//#region Fetch user
+	if ($i && $i.token) {
+		if (_DEV_) {
+			console.log('account cache found. refreshing...');
+		}
+
+		refreshAccount();
+	}
+	//#endregion
+
+	try {
+		await fetchCustomEmojis();
+	} catch (err) { /* empty */ }
+
+	const app = createVue();
+
+	if (_DEV_) {
+		app.config.performance = true;
+	}
+
+	widgets(app);
+	directives(app);
+	components(app);
+
+	// https://github.com/misskey-dev/misskey/pull/8575#issuecomment-1114239210
+	// なぜか2回実行されることがあるため、mountするdivを1つに制限する
+	const rootEl = ((): HTMLElement => {
+		const MISSKEY_MOUNT_DIV_ID = 'misskey_app';
+
+		const currentRoot = document.getElementById(MISSKEY_MOUNT_DIV_ID);
+
+		if (currentRoot) {
+			console.warn('multiple import detected');
+			return currentRoot;
+		}
+
+		const root = document.createElement('div');
+		root.id = MISSKEY_MOUNT_DIV_ID;
+		document.body.appendChild(root);
+		return root;
+	})();
+
+	app.mount(rootEl);
+
+	// boot.jsのやつを解除
+	window.onerror = null;
+	window.onunhandledrejection = null;
+
+	removeSplash();
+
+	return {
+		isClientUpdated,
+		app,
+	};
+}
+
+function removeSplash() {
+	const splash = document.getElementById('splash');
+	if (splash) {
+		splash.style.opacity = '0';
+		splash.style.pointerEvents = 'none';
+	}
+}
diff --git a/packages/frontend/src/boot/main-boot.ts b/packages/frontend/src/boot/main-boot.ts
new file mode 100644
index 0000000000000000000000000000000000000000..c0bfa4603e39ad5c65c5c2cf0c26facfc15a8a45
--- /dev/null
+++ b/packages/frontend/src/boot/main-boot.ts
@@ -0,0 +1,254 @@
+import { computed, createApp, watch, markRaw, version as vueVersion, defineAsyncComponent } from 'vue';
+import { common } from './common';
+import { version, ui, lang, updateLocale } from '@/config';
+import { i18n, updateI18n } from '@/i18n';
+import { confirm, alert, post, popup, toast } from '@/os';
+import { useStream } from '@/stream';
+import * as sound from '@/scripts/sound';
+import { $i, refreshAccount, login, updateAccount, signout } from '@/account';
+import { defaultStore, ColdDeviceStorage } from '@/store';
+import { makeHotkey } from '@/scripts/hotkey';
+import { reactionPicker } from '@/scripts/reaction-picker';
+import { miLocalStorage } from '@/local-storage';
+import { claimAchievement, claimedAchievements } from '@/scripts/achievements';
+import { mainRouter } from '@/router';
+import { initializeSw } from '@/scripts/initialize-sw';
+
+export async function mainBoot() {
+	const { isClientUpdated } = await common(() => createApp(
+		new URLSearchParams(window.location.search).has('zen') ? defineAsyncComponent(() => import('@/ui/zen.vue')) :
+		!$i ? defineAsyncComponent(() => import('@/ui/visitor.vue')) :
+		ui === 'deck' ? defineAsyncComponent(() => import('@/ui/deck.vue')) :
+		ui === 'classic' ? defineAsyncComponent(() => import('@/ui/classic.vue')) :
+		defineAsyncComponent(() => import('@/ui/universal.vue')),
+	));
+
+	reactionPicker.init();
+
+	if (isClientUpdated && $i) {
+		popup(defineAsyncComponent(() => import('@/components/MkUpdated.vue')), {}, {}, 'closed');
+	}
+
+	const stream = useStream();
+
+	let reloadDialogShowing = false;
+	stream.on('_disconnected_', async () => {
+		if (defaultStore.state.serverDisconnectedBehavior === 'reload') {
+			location.reload();
+		} else if (defaultStore.state.serverDisconnectedBehavior === 'dialog') {
+			if (reloadDialogShowing) return;
+			reloadDialogShowing = true;
+			const { canceled } = await confirm({
+				type: 'warning',
+				title: i18n.ts.disconnectedFromServer,
+				text: i18n.ts.reloadConfirm,
+			});
+			reloadDialogShowing = false;
+			if (!canceled) {
+				location.reload();
+			}
+		}
+	});
+
+	for (const plugin of ColdDeviceStorage.get('plugins').filter(p => p.active)) {
+		import('../plugin').then(async ({ install }) => {
+			// Workaround for https://bugs.webkit.org/show_bug.cgi?id=242740
+			await new Promise(r => setTimeout(r, 0));
+			install(plugin);
+		});
+	}
+
+	const hotkeys = {
+		'd': (): void => {
+			defaultStore.set('darkMode', !defaultStore.state.darkMode);
+		},
+		's': (): void => {
+			mainRouter.push('/search');
+		},
+	};
+
+	if ($i) {
+		// only add post shortcuts if logged in
+		hotkeys['p|n'] = post;
+
+		defaultStore.loaded.then(() => {
+			if (defaultStore.state.accountSetupWizard !== -1) {
+				popup(defineAsyncComponent(() => import('@/components/MkUserSetupDialog.vue')), {}, {}, 'closed');
+			}
+		});
+
+		if ($i.isDeleted) {
+			alert({
+				type: 'warning',
+				text: i18n.ts.accountDeletionInProgress,
+			});
+		}
+
+		const now = new Date();
+		const m = now.getMonth() + 1;
+		const d = now.getDate();
+		
+		if ($i.birthday) {
+			const bm = parseInt($i.birthday.split('-')[1]);
+			const bd = parseInt($i.birthday.split('-')[2]);
+			if (m === bm && d === bd) {
+				claimAchievement('loggedInOnBirthday');
+			}
+		}
+
+		if (m === 1 && d === 1) {
+			claimAchievement('loggedInOnNewYearsDay');
+		}
+
+		if ($i.loggedInDays >= 3) claimAchievement('login3');
+		if ($i.loggedInDays >= 7) claimAchievement('login7');
+		if ($i.loggedInDays >= 15) claimAchievement('login15');
+		if ($i.loggedInDays >= 30) claimAchievement('login30');
+		if ($i.loggedInDays >= 60) claimAchievement('login60');
+		if ($i.loggedInDays >= 100) claimAchievement('login100');
+		if ($i.loggedInDays >= 200) claimAchievement('login200');
+		if ($i.loggedInDays >= 300) claimAchievement('login300');
+		if ($i.loggedInDays >= 400) claimAchievement('login400');
+		if ($i.loggedInDays >= 500) claimAchievement('login500');
+		if ($i.loggedInDays >= 600) claimAchievement('login600');
+		if ($i.loggedInDays >= 700) claimAchievement('login700');
+		if ($i.loggedInDays >= 800) claimAchievement('login800');
+		if ($i.loggedInDays >= 900) claimAchievement('login900');
+		if ($i.loggedInDays >= 1000) claimAchievement('login1000');
+
+		if ($i.notesCount > 0) claimAchievement('notes1');
+		if ($i.notesCount >= 10) claimAchievement('notes10');
+		if ($i.notesCount >= 100) claimAchievement('notes100');
+		if ($i.notesCount >= 500) claimAchievement('notes500');
+		if ($i.notesCount >= 1000) claimAchievement('notes1000');
+		if ($i.notesCount >= 5000) claimAchievement('notes5000');
+		if ($i.notesCount >= 10000) claimAchievement('notes10000');
+		if ($i.notesCount >= 20000) claimAchievement('notes20000');
+		if ($i.notesCount >= 30000) claimAchievement('notes30000');
+		if ($i.notesCount >= 40000) claimAchievement('notes40000');
+		if ($i.notesCount >= 50000) claimAchievement('notes50000');
+		if ($i.notesCount >= 60000) claimAchievement('notes60000');
+		if ($i.notesCount >= 70000) claimAchievement('notes70000');
+		if ($i.notesCount >= 80000) claimAchievement('notes80000');
+		if ($i.notesCount >= 90000) claimAchievement('notes90000');
+		if ($i.notesCount >= 100000) claimAchievement('notes100000');
+
+		if ($i.followersCount > 0) claimAchievement('followers1');
+		if ($i.followersCount >= 10) claimAchievement('followers10');
+		if ($i.followersCount >= 50) claimAchievement('followers50');
+		if ($i.followersCount >= 100) claimAchievement('followers100');
+		if ($i.followersCount >= 300) claimAchievement('followers300');
+		if ($i.followersCount >= 500) claimAchievement('followers500');
+		if ($i.followersCount >= 1000) claimAchievement('followers1000');
+
+		if (Date.now() - new Date($i.createdAt).getTime() > 1000 * 60 * 60 * 24 * 365) {
+			claimAchievement('passedSinceAccountCreated1');
+		}
+		if (Date.now() - new Date($i.createdAt).getTime() > 1000 * 60 * 60 * 24 * 365 * 2) {
+			claimAchievement('passedSinceAccountCreated2');
+		}
+		if (Date.now() - new Date($i.createdAt).getTime() > 1000 * 60 * 60 * 24 * 365 * 3) {
+			claimAchievement('passedSinceAccountCreated3');
+		}
+
+		if (claimedAchievements.length >= 30) {
+			claimAchievement('collectAchievements30');
+		}
+
+		window.setInterval(() => {
+			if (Math.floor(Math.random() * 20000) === 0) {
+				claimAchievement('justPlainLucky');
+			}
+		}, 1000 * 10);
+
+		window.setTimeout(() => {
+			claimAchievement('client30min');
+		}, 1000 * 60 * 30);
+
+		window.setTimeout(() => {
+			claimAchievement('client60min');
+		}, 1000 * 60 * 60);
+
+		const lastUsed = miLocalStorage.getItem('lastUsed');
+		if (lastUsed) {
+			const lastUsedDate = parseInt(lastUsed, 10);
+			// 二時間以上前なら
+			if (Date.now() - lastUsedDate > 1000 * 60 * 60 * 2) {
+				toast(i18n.t('welcomeBackWithName', {
+					name: $i.name || $i.username,
+				}));
+			}
+		}
+		miLocalStorage.setItem('lastUsed', Date.now().toString());
+
+		const latestDonationInfoShownAt = miLocalStorage.getItem('latestDonationInfoShownAt');
+		const neverShowDonationInfo = miLocalStorage.getItem('neverShowDonationInfo');
+		if (neverShowDonationInfo !== 'true' && (new Date($i.createdAt).getTime() < (Date.now() - (1000 * 60 * 60 * 24 * 3))) && !location.pathname.startsWith('/miauth')) {
+			if (latestDonationInfoShownAt == null || (new Date(latestDonationInfoShownAt).getTime() < (Date.now() - (1000 * 60 * 60 * 24 * 30)))) {
+				popup(defineAsyncComponent(() => import('@/components/MkDonation.vue')), {}, {}, 'closed');
+			}
+		}
+
+		if ('Notification' in window) {
+			// 許可を得ていなかったらリクエスト
+			if (Notification.permission === 'default') {
+				Notification.requestPermission();
+			}
+		}
+
+		const main = markRaw(stream.useChannel('main', null, 'System'));
+
+		// 自分の情報が更新されたとき
+		main.on('meUpdated', i => {
+			updateAccount(i);
+		});
+
+		main.on('readAllNotifications', () => {
+			updateAccount({ hasUnreadNotification: false });
+		});
+
+		main.on('unreadNotification', () => {
+			updateAccount({ hasUnreadNotification: true });
+		});
+
+		main.on('unreadMention', () => {
+			updateAccount({ hasUnreadMentions: true });
+		});
+
+		main.on('readAllUnreadMentions', () => {
+			updateAccount({ hasUnreadMentions: false });
+		});
+
+		main.on('unreadSpecifiedNote', () => {
+			updateAccount({ hasUnreadSpecifiedNotes: true });
+		});
+
+		main.on('readAllUnreadSpecifiedNotes', () => {
+			updateAccount({ hasUnreadSpecifiedNotes: false });
+		});
+
+		main.on('readAllAntennas', () => {
+			updateAccount({ hasUnreadAntenna: false });
+		});
+
+		main.on('unreadAntenna', () => {
+			updateAccount({ hasUnreadAntenna: true });
+			sound.play('antenna');
+		});
+
+		main.on('readAllAnnouncements', () => {
+			updateAccount({ hasUnreadAnnouncement: false });
+		});
+
+		// トークンが再生成されたとき
+		// このままではMisskeyが利用できないので強制的にサインアウトさせる
+		main.on('myTokenRegenerated', () => {
+			signout();
+		});
+	}
+
+	// shortcut
+	document.addEventListener('keydown', makeHotkey(hotkeys));
+
+	initializeSw();
+}
diff --git a/packages/frontend/src/boot/sub-boot.ts b/packages/frontend/src/boot/sub-boot.ts
new file mode 100644
index 0000000000000000000000000000000000000000..c2664f6c1d3e0e2ea9fa8e47b0c4392a5533a3a2
--- /dev/null
+++ b/packages/frontend/src/boot/sub-boot.ts
@@ -0,0 +1,8 @@
+import { computed, createApp, watch, markRaw, version as vueVersion, defineAsyncComponent } from 'vue';
+import { common } from './common';
+
+export async function subBoot() {
+	const { isClientUpdated } = await common(() => createApp(
+		defineAsyncComponent(() => import('@/ui/minimum.vue')),
+	));
+}
diff --git a/packages/frontend/src/components/MkDrive.vue b/packages/frontend/src/components/MkDrive.vue
index bfec57d6a04ea4e146b1537f105cbef166bb5a30..edeaabfba4bb13bfbe6bbe1468004e996e03a478 100644
--- a/packages/frontend/src/components/MkDrive.vue
+++ b/packages/frontend/src/components/MkDrive.vue
@@ -95,7 +95,7 @@ import XNavFolder from '@/components/MkDrive.navFolder.vue';
 import XFolder from '@/components/MkDrive.folder.vue';
 import XFile from '@/components/MkDrive.file.vue';
 import * as os from '@/os';
-import { stream } from '@/stream';
+import { useStream } from '@/stream';
 import { defaultStore } from '@/store';
 import { i18n } from '@/i18n';
 import { uploadFile, uploads } from '@/scripts/upload';
@@ -131,7 +131,7 @@ const hierarchyFolders = ref<Misskey.entities.DriveFolder[]>([]);
 const selectedFiles = ref<Misskey.entities.DriveFile[]>([]);
 const selectedFolders = ref<Misskey.entities.DriveFolder[]>([]);
 const uploadings = uploads;
-const connection = stream.useChannel('drive');
+const connection = useStream().useChannel('drive');
 const keepOriginal = ref<boolean>(defaultStore.state.keepOriginalUploading); // 外部渡しが多いので$refは使わないほうがよい
 
 // ドロップされようとしているか
diff --git a/packages/frontend/src/components/MkFollowButton.vue b/packages/frontend/src/components/MkFollowButton.vue
index beee21c647c3f83f55450b6bc02e9a3d2b7ed9e1..7d066fd033c04ecd538541bfc448f80531bac894 100644
--- a/packages/frontend/src/components/MkFollowButton.vue
+++ b/packages/frontend/src/components/MkFollowButton.vue
@@ -33,7 +33,7 @@
 import { onBeforeUnmount, onMounted } from 'vue';
 import * as Misskey from 'misskey-js';
 import * as os from '@/os';
-import { stream } from '@/stream';
+import { useStream } from '@/stream';
 import { i18n } from '@/i18n';
 import { claimAchievement } from '@/scripts/achievements';
 import { $i } from '@/account';
@@ -50,7 +50,7 @@ const props = withDefaults(defineProps<{
 let isFollowing = $ref(props.user.isFollowing);
 let hasPendingFollowRequestFromYou = $ref(props.user.hasPendingFollowRequestFromYou);
 let wait = $ref(false);
-const connection = stream.useChannel('main');
+const connection = useStream().useChannel('main');
 
 if (props.user.isFollowing == null) {
 	os.api('users/show', {
diff --git a/packages/frontend/src/components/MkNotifications.vue b/packages/frontend/src/components/MkNotifications.vue
index 1aea95fe0e0d7177ed70cc7409857a877635e988..8cb1b064ba65b750277bd802e30900ceb6468008 100644
--- a/packages/frontend/src/components/MkNotifications.vue
+++ b/packages/frontend/src/components/MkNotifications.vue
@@ -22,7 +22,7 @@ import MkPagination, { Paging } from '@/components/MkPagination.vue';
 import XNotification from '@/components/MkNotification.vue';
 import MkDateSeparatedList from '@/components/MkDateSeparatedList.vue';
 import MkNote from '@/components/MkNote.vue';
-import { stream } from '@/stream';
+import { useStream } from '@/stream';
 import { $i } from '@/account';
 import { i18n } from '@/i18n';
 import { notificationTypes } from '@/const';
@@ -45,7 +45,7 @@ const pagination: Paging = {
 const onNotification = (notification) => {
 	const isMuted = props.includeTypes ? !props.includeTypes.includes(notification.type) : $i.mutingNotificationTypes.includes(notification.type);
 	if (isMuted || document.visibilityState === 'visible') {
-		stream.send('readNotification');
+		useStream().send('readNotification');
 	}
 
 	if (!isMuted) {
@@ -56,7 +56,7 @@ const onNotification = (notification) => {
 let connection;
 
 onMounted(() => {
-	connection = stream.useChannel('main');
+	connection = useStream().useChannel('main');
 	connection.on('notification', onNotification);
 });
 
diff --git a/packages/frontend/src/components/MkTimeline.vue b/packages/frontend/src/components/MkTimeline.vue
index fb0a3a4b67d823449f683ef54847f7db642e653a..00a0cb760d8d27b0ae56ff34c4d9689f7585aeb6 100644
--- a/packages/frontend/src/components/MkTimeline.vue
+++ b/packages/frontend/src/components/MkTimeline.vue
@@ -5,7 +5,7 @@
 <script lang="ts" setup>
 import { computed, provide, onUnmounted } from 'vue';
 import MkNotes from '@/components/MkNotes.vue';
-import { stream } from '@/stream';
+import { useStream } from '@/stream';
 import * as sound from '@/scripts/sound';
 import { $i } from '@/account';
 import { defaultStore } from '@/store';
@@ -57,6 +57,8 @@ let query;
 let connection;
 let connection2;
 
+const stream = useStream();
+
 if (props.src === 'antenna') {
 	endpoint = 'antennas/notes';
 	query = {
diff --git a/packages/frontend/src/custom-emojis.ts b/packages/frontend/src/custom-emojis.ts
index a89a420d77b8026e192d26aa16ab3f51b502d40e..de1b5b8a6314c3cec3de0524fe446e1160fdbeb8 100644
--- a/packages/frontend/src/custom-emojis.ts
+++ b/packages/frontend/src/custom-emojis.ts
@@ -1,8 +1,7 @@
 import { shallowRef, computed, markRaw } from 'vue';
 import * as Misskey from 'misskey-js';
 import { api, apiGet } from './os';
-import { miLocalStorage } from './local-storage';
-import { stream } from '@/stream';
+import { useStream } from '@/stream';
 import { get, set } from '@/scripts/idb-proxy';
 
 const storageCache = await get('emojis');
@@ -17,6 +16,9 @@ export const customEmojiCategories = computed<[ ...string[], null ]>(() => {
 	return markRaw([...Array.from(categories), null]);
 });
 
+// TODO: ここら辺副作用なのでいい感じにする
+const stream = useStream();
+
 stream.on('emojiAdded', emojiData => {
 	customEmojis.value = [emojiData.emoji, ...customEmojis.value];
 	set('emojis', customEmojis.value);
@@ -34,10 +36,9 @@ stream.on('emojiDeleted', emojiData => {
 
 export async function fetchCustomEmojis(force = false) {
 	const now = Date.now();
-	const needsMigration = miLocalStorage.getItem('emojis') != null;
 
 	let res;
-	if (force || needsMigration) {
+	if (force) {
 		res = await api('emojis', {});
 	} else {
 		const lastFetchedAt = await get('lastEmojisFetchedAt');
@@ -48,10 +49,6 @@ export async function fetchCustomEmojis(force = false) {
 	customEmojis.value = res.emojis;
 	set('emojis', res.emojis);
 	set('lastEmojisFetchedAt', now);
-	if (needsMigration) {
-		miLocalStorage.removeItem('emojis');
-		miLocalStorage.removeItem('lastEmojisFetchedAt');
-	}
 }
 
 let cachedTags;
diff --git a/packages/frontend/src/init.ts b/packages/frontend/src/init.ts
deleted file mode 100644
index 763b6d44b0294ecb56f6094504f02e44ac22aa4d..0000000000000000000000000000000000000000
--- a/packages/frontend/src/init.ts
+++ /dev/null
@@ -1,523 +0,0 @@
-/**
- * Client entry point
- */
-// https://vitejs.dev/config/build-options.html#build-modulepreload
-import 'vite/modulepreload-polyfill';
-
-import '@/style.scss';
-
-import { computed, createApp, watch, markRaw, version as vueVersion, defineAsyncComponent } from 'vue';
-import { compareVersions } from 'compare-versions';
-import JSON5 from 'json5';
-
-import widgets from '@/widgets';
-import directives from '@/directives';
-import components from '@/components';
-import { version, ui, lang, updateLocale } from '@/config';
-import { applyTheme } from '@/scripts/theme';
-import { isDeviceDarkmode } from '@/scripts/is-device-darkmode';
-import { i18n, updateI18n } from '@/i18n';
-import { confirm, alert, post, popup, toast } from '@/os';
-import { stream } from '@/stream';
-import * as sound from '@/scripts/sound';
-import { $i, refreshAccount, login, updateAccount, signout } from '@/account';
-import { defaultStore, ColdDeviceStorage } from '@/store';
-import { fetchInstance, instance } from '@/instance';
-import { makeHotkey } from '@/scripts/hotkey';
-import { deviceKind } from '@/scripts/device-kind';
-import { initializeSw } from '@/scripts/initialize-sw';
-import { reloadChannel } from '@/scripts/unison-reload';
-import { reactionPicker } from '@/scripts/reaction-picker';
-import { getUrlWithoutLoginId } from '@/scripts/login-id';
-import { getAccountFromId } from '@/scripts/get-account-from-id';
-import { deckStore } from '@/ui/deck/deck-store';
-import { miLocalStorage } from '@/local-storage';
-import { claimAchievement, claimedAchievements } from '@/scripts/achievements';
-import { fetchCustomEmojis } from '@/custom-emojis';
-import { mainRouter } from '@/router';
-
-console.info(`Misskey v${version}`);
-
-if (_DEV_) {
-	console.warn('Development mode!!!');
-
-	console.info(`vue ${vueVersion}`);
-
-	// eslint-disable-next-line @typescript-eslint/no-explicit-any
-	(window as any).$i = $i;
-	// eslint-disable-next-line @typescript-eslint/no-explicit-any
-	(window as any).$store = defaultStore;
-
-	window.addEventListener('error', event => {
-		console.error(event);
-		/*
-		alert({
-			type: 'error',
-			title: 'DEV: Unhandled error',
-			text: event.message
-		});
-		*/
-	});
-
-	window.addEventListener('unhandledrejection', event => {
-		console.error(event);
-		/*
-		alert({
-			type: 'error',
-			title: 'DEV: Unhandled promise rejection',
-			text: event.reason
-		});
-		*/
-	});
-}
-
-//#region Detect language & fetch translations
-const localeVersion = miLocalStorage.getItem('localeVersion');
-const localeOutdated = (localeVersion == null || localeVersion !== version);
-if (localeOutdated) {
-	const res = await window.fetch(`/assets/locales/${lang}.${version}.json`);
-	if (res.status === 200) {
-		const newLocale = await res.text();
-		const parsedNewLocale = JSON.parse(newLocale);
-		miLocalStorage.setItem('locale', newLocale);
-		miLocalStorage.setItem('localeVersion', version);
-		updateLocale(parsedNewLocale);
-		updateI18n(parsedNewLocale);
-	}
-}
-//#endregion
-
-// タッチデバイスでCSSの:hoverを機能させる
-document.addEventListener('touchend', () => {}, { passive: true });
-
-// 一斉リロード
-reloadChannel.addEventListener('message', path => {
-	if (path !== null) location.href = path;
-	else location.reload();
-});
-
-// If mobile, insert the viewport meta tag
-if (['smartphone', 'tablet'].includes(deviceKind)) {
-	const viewport = document.getElementsByName('viewport').item(0);
-	viewport.setAttribute('content',
-		`${viewport.getAttribute('content')}, minimum-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover`);
-}
-
-//#region Set lang attr
-const html = document.documentElement;
-html.setAttribute('lang', lang);
-//#endregion
-
-//#region loginId
-const params = new URLSearchParams(location.search);
-const loginId = params.get('loginId');
-
-if (loginId) {
-	const target = getUrlWithoutLoginId(location.href);
-
-	if (!$i || $i.id !== loginId) {
-		const account = await getAccountFromId(loginId);
-		if (account) {
-			await login(account.token, target);
-		}
-	}
-
-	history.replaceState({ misskey: 'loginId' }, '', target);
-}
-
-//#endregion
-
-//#region Fetch user
-if ($i && $i.token) {
-	if (_DEV_) {
-		console.log('account cache found. refreshing...');
-	}
-
-	refreshAccount();
-} else {
-	if (_DEV_) {
-		console.log('no account cache found.');
-	}
-
-	// 連携ログインの場合用にCookieを参照する
-	const i = (document.cookie.match(/igi=(\w+)/) ?? [null, null])[1];
-
-	if (i != null && i !== 'null') {
-		if (_DEV_) {
-			console.log('signing...');
-		}
-
-		try {
-			document.body.innerHTML = '<div>Please wait...</div>';
-			await login(i);
-		} catch (err) {
-			// Render the error screen
-			// TODO: ちゃんとしたコンポーネントをレンダリングする(v10とかのトラブルシューティングゲーム付きのやつみたいな)
-			document.body.innerHTML = '<div id="err">Oops!</div>';
-		}
-	} else {
-		if (_DEV_) {
-			console.log('not signed in');
-		}
-	}
-}
-//#endregion
-
-const fetchInstanceMetaPromise = fetchInstance();
-
-fetchInstanceMetaPromise.then(() => {
-	miLocalStorage.setItem('v', instance.version);
-
-	// Init service worker
-	initializeSw();
-});
-
-try {
-	await fetchCustomEmojis();
-} catch (err) { /* empty */ }
-
-const app = createApp(
-	new URLSearchParams(window.location.search).has('zen') ? defineAsyncComponent(() => import('@/ui/zen.vue')) :
-	!$i ? defineAsyncComponent(() => import('@/ui/visitor.vue')) :
-	ui === 'deck' ? defineAsyncComponent(() => import('@/ui/deck.vue')) :
-	ui === 'classic' ? defineAsyncComponent(() => import('@/ui/classic.vue')) :
-	defineAsyncComponent(() => import('@/ui/universal.vue')),
-);
-
-if (_DEV_) {
-	app.config.performance = true;
-}
-
-widgets(app);
-directives(app);
-components(app);
-
-const splash = document.getElementById('splash');
-// 念のためnullチェック(HTMLが古い場合があるため(そのうち消す))
-if (splash) splash.addEventListener('transitionend', () => {
-	splash.remove();
-});
-
-await deckStore.ready;
-
-// https://github.com/misskey-dev/misskey/pull/8575#issuecomment-1114239210
-// なぜかinit.tsの内容が2回実行されることがあるため、mountするdivを1つに制限する
-const rootEl = ((): HTMLElement => {
-	const MISSKEY_MOUNT_DIV_ID = 'misskey_app';
-
-	const currentRoot = document.getElementById(MISSKEY_MOUNT_DIV_ID);
-
-	if (currentRoot) {
-		console.warn('multiple import detected');
-		return currentRoot;
-	}
-
-	const root = document.createElement('div');
-	root.id = MISSKEY_MOUNT_DIV_ID;
-	document.body.appendChild(root);
-	return root;
-})();
-
-app.mount(rootEl);
-
-// boot.jsのやつを解除
-window.onerror = null;
-window.onunhandledrejection = null;
-
-reactionPicker.init();
-
-if (splash) {
-	splash.style.opacity = '0';
-	splash.style.pointerEvents = 'none';
-}
-
-// クライアントが更新されたか?
-const lastVersion = miLocalStorage.getItem('lastVersion');
-if (lastVersion !== version) {
-	miLocalStorage.setItem('lastVersion', version);
-
-	// テーマリビルドするため
-	miLocalStorage.removeItem('theme');
-
-	try { // 変なバージョン文字列来るとcompareVersionsでエラーになるため
-		if (lastVersion != null && compareVersions(version, lastVersion) === 1) {
-			// ログインしてる場合だけ
-			if ($i) {
-				popup(defineAsyncComponent(() => import('@/components/MkUpdated.vue')), {}, {}, 'closed');
-			}
-		}
-	} catch (err) { /* empty */ }
-}
-
-await defaultStore.ready;
-
-// NOTE: この処理は必ず↑のクライアント更新時処理より後に来ること(テーマ再構築のため)
-watch(defaultStore.reactiveState.darkMode, (darkMode) => {
-	applyTheme(darkMode ? ColdDeviceStorage.get('darkTheme') : ColdDeviceStorage.get('lightTheme'));
-}, { immediate: miLocalStorage.getItem('theme') == null });
-
-const darkTheme = computed(ColdDeviceStorage.makeGetterSetter('darkTheme'));
-const lightTheme = computed(ColdDeviceStorage.makeGetterSetter('lightTheme'));
-
-watch(darkTheme, (theme) => {
-	if (defaultStore.state.darkMode) {
-		applyTheme(theme);
-	}
-});
-
-watch(lightTheme, (theme) => {
-	if (!defaultStore.state.darkMode) {
-		applyTheme(theme);
-	}
-});
-
-//#region Sync dark mode
-if (ColdDeviceStorage.get('syncDeviceDarkMode')) {
-	defaultStore.set('darkMode', isDeviceDarkmode());
-}
-
-window.matchMedia('(prefers-color-scheme: dark)').addListener(mql => {
-	if (ColdDeviceStorage.get('syncDeviceDarkMode')) {
-		defaultStore.set('darkMode', mql.matches);
-	}
-});
-//#endregion
-
-fetchInstanceMetaPromise.then(() => {
-	if (defaultStore.state.themeInitial) {
-		if (instance.defaultLightTheme != null) ColdDeviceStorage.set('lightTheme', JSON5.parse(instance.defaultLightTheme));
-		if (instance.defaultDarkTheme != null) ColdDeviceStorage.set('darkTheme', JSON5.parse(instance.defaultDarkTheme));
-		defaultStore.set('themeInitial', false);
-	}
-});
-
-watch(defaultStore.reactiveState.useBlurEffectForModal, v => {
-	document.documentElement.style.setProperty('--modalBgFilter', v ? 'blur(4px)' : 'none');
-}, { immediate: true });
-
-watch(defaultStore.reactiveState.useBlurEffect, v => {
-	if (v) {
-		document.documentElement.style.removeProperty('--blur');
-	} else {
-		document.documentElement.style.setProperty('--blur', 'none');
-	}
-}, { immediate: true });
-
-let reloadDialogShowing = false;
-stream.on('_disconnected_', async () => {
-	if (defaultStore.state.serverDisconnectedBehavior === 'reload') {
-		location.reload();
-	} else if (defaultStore.state.serverDisconnectedBehavior === 'dialog') {
-		if (reloadDialogShowing) return;
-		reloadDialogShowing = true;
-		const { canceled } = await confirm({
-			type: 'warning',
-			title: i18n.ts.disconnectedFromServer,
-			text: i18n.ts.reloadConfirm,
-		});
-		reloadDialogShowing = false;
-		if (!canceled) {
-			location.reload();
-		}
-	}
-});
-
-for (const plugin of ColdDeviceStorage.get('plugins').filter(p => p.active)) {
-	import('./plugin').then(async ({ install }) => {
-		// Workaround for https://bugs.webkit.org/show_bug.cgi?id=242740
-		await new Promise(r => setTimeout(r, 0));
-		install(plugin);
-	});
-}
-
-const hotkeys = {
-	'd': (): void => {
-		defaultStore.set('darkMode', !defaultStore.state.darkMode);
-	},
-	's': (): void => {
-		mainRouter.push('/search');
-	},
-};
-
-if ($i) {
-	// only add post shortcuts if logged in
-	hotkeys['p|n'] = post;
-
-	defaultStore.loaded.then(() => {
-		if (defaultStore.state.accountSetupWizard !== -1) {
-			popup(defineAsyncComponent(() => import('@/components/MkUserSetupDialog.vue')), {}, {}, 'closed');
-		}
-	});
-
-	if ($i.isDeleted) {
-		alert({
-			type: 'warning',
-			text: i18n.ts.accountDeletionInProgress,
-		});
-	}
-
-	const now = new Date();
-	const m = now.getMonth() + 1;
-	const d = now.getDate();
-	
-	if ($i.birthday) {
-		const bm = parseInt($i.birthday.split('-')[1]);
-		const bd = parseInt($i.birthday.split('-')[2]);
-		if (m === bm && d === bd) {
-			claimAchievement('loggedInOnBirthday');
-		}
-	}
-
-	if (m === 1 && d === 1) {
-		claimAchievement('loggedInOnNewYearsDay');
-	}
-
-	if ($i.loggedInDays >= 3) claimAchievement('login3');
-	if ($i.loggedInDays >= 7) claimAchievement('login7');
-	if ($i.loggedInDays >= 15) claimAchievement('login15');
-	if ($i.loggedInDays >= 30) claimAchievement('login30');
-	if ($i.loggedInDays >= 60) claimAchievement('login60');
-	if ($i.loggedInDays >= 100) claimAchievement('login100');
-	if ($i.loggedInDays >= 200) claimAchievement('login200');
-	if ($i.loggedInDays >= 300) claimAchievement('login300');
-	if ($i.loggedInDays >= 400) claimAchievement('login400');
-	if ($i.loggedInDays >= 500) claimAchievement('login500');
-	if ($i.loggedInDays >= 600) claimAchievement('login600');
-	if ($i.loggedInDays >= 700) claimAchievement('login700');
-	if ($i.loggedInDays >= 800) claimAchievement('login800');
-	if ($i.loggedInDays >= 900) claimAchievement('login900');
-	if ($i.loggedInDays >= 1000) claimAchievement('login1000');
-
-	if ($i.notesCount > 0) claimAchievement('notes1');
-	if ($i.notesCount >= 10) claimAchievement('notes10');
-	if ($i.notesCount >= 100) claimAchievement('notes100');
-	if ($i.notesCount >= 500) claimAchievement('notes500');
-	if ($i.notesCount >= 1000) claimAchievement('notes1000');
-	if ($i.notesCount >= 5000) claimAchievement('notes5000');
-	if ($i.notesCount >= 10000) claimAchievement('notes10000');
-	if ($i.notesCount >= 20000) claimAchievement('notes20000');
-	if ($i.notesCount >= 30000) claimAchievement('notes30000');
-	if ($i.notesCount >= 40000) claimAchievement('notes40000');
-	if ($i.notesCount >= 50000) claimAchievement('notes50000');
-	if ($i.notesCount >= 60000) claimAchievement('notes60000');
-	if ($i.notesCount >= 70000) claimAchievement('notes70000');
-	if ($i.notesCount >= 80000) claimAchievement('notes80000');
-	if ($i.notesCount >= 90000) claimAchievement('notes90000');
-	if ($i.notesCount >= 100000) claimAchievement('notes100000');
-
-	if ($i.followersCount > 0) claimAchievement('followers1');
-	if ($i.followersCount >= 10) claimAchievement('followers10');
-	if ($i.followersCount >= 50) claimAchievement('followers50');
-	if ($i.followersCount >= 100) claimAchievement('followers100');
-	if ($i.followersCount >= 300) claimAchievement('followers300');
-	if ($i.followersCount >= 500) claimAchievement('followers500');
-	if ($i.followersCount >= 1000) claimAchievement('followers1000');
-
-	if (Date.now() - new Date($i.createdAt).getTime() > 1000 * 60 * 60 * 24 * 365) {
-		claimAchievement('passedSinceAccountCreated1');
-	}
-	if (Date.now() - new Date($i.createdAt).getTime() > 1000 * 60 * 60 * 24 * 365 * 2) {
-		claimAchievement('passedSinceAccountCreated2');
-	}
-	if (Date.now() - new Date($i.createdAt).getTime() > 1000 * 60 * 60 * 24 * 365 * 3) {
-		claimAchievement('passedSinceAccountCreated3');
-	}
-
-	if (claimedAchievements.length >= 30) {
-		claimAchievement('collectAchievements30');
-	}
-
-	window.setInterval(() => {
-		if (Math.floor(Math.random() * 20000) === 0) {
-			claimAchievement('justPlainLucky');
-		}
-	}, 1000 * 10);
-
-	window.setTimeout(() => {
-		claimAchievement('client30min');
-	}, 1000 * 60 * 30);
-
-	window.setTimeout(() => {
-		claimAchievement('client60min');
-	}, 1000 * 60 * 60);
-
-	const lastUsed = miLocalStorage.getItem('lastUsed');
-	if (lastUsed) {
-		const lastUsedDate = parseInt(lastUsed, 10);
-		// 二時間以上前なら
-		if (Date.now() - lastUsedDate > 1000 * 60 * 60 * 2) {
-			toast(i18n.t('welcomeBackWithName', {
-				name: $i.name || $i.username,
-			}));
-		}
-	}
-	miLocalStorage.setItem('lastUsed', Date.now().toString());
-
-	const latestDonationInfoShownAt = miLocalStorage.getItem('latestDonationInfoShownAt');
-	const neverShowDonationInfo = miLocalStorage.getItem('neverShowDonationInfo');
-	if (neverShowDonationInfo !== 'true' && (new Date($i.createdAt).getTime() < (Date.now() - (1000 * 60 * 60 * 24 * 3))) && !location.pathname.startsWith('/miauth')) {
-		if (latestDonationInfoShownAt == null || (new Date(latestDonationInfoShownAt).getTime() < (Date.now() - (1000 * 60 * 60 * 24 * 30)))) {
-			popup(defineAsyncComponent(() => import('@/components/MkDonation.vue')), {}, {}, 'closed');
-		}
-	}
-
-	if ('Notification' in window) {
-		// 許可を得ていなかったらリクエスト
-		if (Notification.permission === 'default') {
-			Notification.requestPermission();
-		}
-	}
-
-	const main = markRaw(stream.useChannel('main', null, 'System'));
-
-	// 自分の情報が更新されたとき
-	main.on('meUpdated', i => {
-		updateAccount(i);
-	});
-
-	main.on('readAllNotifications', () => {
-		updateAccount({ hasUnreadNotification: false });
-	});
-
-	main.on('unreadNotification', () => {
-		updateAccount({ hasUnreadNotification: true });
-	});
-
-	main.on('unreadMention', () => {
-		updateAccount({ hasUnreadMentions: true });
-	});
-
-	main.on('readAllUnreadMentions', () => {
-		updateAccount({ hasUnreadMentions: false });
-	});
-
-	main.on('unreadSpecifiedNote', () => {
-		updateAccount({ hasUnreadSpecifiedNotes: true });
-	});
-
-	main.on('readAllUnreadSpecifiedNotes', () => {
-		updateAccount({ hasUnreadSpecifiedNotes: false });
-	});
-
-	main.on('readAllAntennas', () => {
-		updateAccount({ hasUnreadAntenna: false });
-	});
-
-	main.on('unreadAntenna', () => {
-		updateAccount({ hasUnreadAntenna: true });
-		sound.play('antenna');
-	});
-
-	main.on('readAllAnnouncements', () => {
-		updateAccount({ hasUnreadAnnouncement: false });
-	});
-
-	// トークンが再生成されたとき
-	// このままではMisskeyが利用できないので強制的にサインアウトさせる
-	main.on('myTokenRegenerated', () => {
-		signout();
-	});
-}
-
-// shortcut
-document.addEventListener('keydown', makeHotkey(hotkeys));
diff --git a/packages/frontend/src/pages/admin/overview.queue.vue b/packages/frontend/src/pages/admin/overview.queue.vue
index 1f56a2826aca0ac8ae6c14853b7f680f74b3a29e..69ca89e226a7f2ecadf2558c8a0e305ec1244dff 100644
--- a/packages/frontend/src/pages/admin/overview.queue.vue
+++ b/packages/frontend/src/pages/admin/overview.queue.vue
@@ -33,9 +33,9 @@
 import { markRaw, onMounted, onUnmounted, ref } from 'vue';
 import XChart from './overview.queue.chart.vue';
 import number from '@/filters/number';
-import { stream } from '@/stream';
+import { useStream } from '@/stream';
 
-const connection = markRaw(stream.useChannel('queueStats'));
+const connection = markRaw(useStream().useChannel('queueStats'));
 
 const activeSincePrevTick = ref(0);
 const active = ref(0);
diff --git a/packages/frontend/src/pages/admin/overview.vue b/packages/frontend/src/pages/admin/overview.vue
index bdfb200a887ca2159fea9f98d7dbfac5c9a786a4..46a93ac5d801ea8c5dcaab3a70ab76bedf1fe5b5 100644
--- a/packages/frontend/src/pages/admin/overview.vue
+++ b/packages/frontend/src/pages/admin/overview.vue
@@ -72,7 +72,7 @@ import XRetention from './overview.retention.vue';
 import XModerators from './overview.moderators.vue';
 import XHeatmap from './overview.heatmap.vue';
 import * as os from '@/os';
-import { stream } from '@/stream';
+import { useStream } from '@/stream';
 import { i18n } from '@/i18n';
 import { definePageMetadata } from '@/scripts/page-metadata';
 import MkFoldableSection from '@/components/MkFoldableSection.vue';
@@ -87,7 +87,7 @@ let federationSubActive = $ref<number | null>(null);
 let federationSubActiveDiff = $ref<number | null>(null);
 let newUsers = $ref(null);
 let activeInstances = $shallowRef(null);
-const queueStatsConnection = markRaw(stream.useChannel('queueStats'));
+const queueStatsConnection = markRaw(useStream().useChannel('queueStats'));
 const now = new Date();
 const filesPagination = {
 	endpoint: 'admin/drive/files' as const,
diff --git a/packages/frontend/src/pages/admin/queue.chart.vue b/packages/frontend/src/pages/admin/queue.chart.vue
index 100d1ea545892cf3eb25e1badd6992b1a681e14b..728f3b2c80297c3ac2e51d57eaba9b61d2865d64 100644
--- a/packages/frontend/src/pages/admin/queue.chart.vue
+++ b/packages/frontend/src/pages/admin/queue.chart.vue
@@ -47,11 +47,11 @@ import { markRaw, onMounted, onUnmounted, ref } from 'vue';
 import XChart from './queue.chart.chart.vue';
 import number from '@/filters/number';
 import * as os from '@/os';
-import { stream } from '@/stream';
+import { useStream } from '@/stream';
 import { i18n } from '@/i18n';
 import MkFolder from '@/components/MkFolder.vue';
 
-const connection = markRaw(stream.useChannel('queueStats'));
+const connection = markRaw(useStream().useChannel('queueStats'));
 
 const activeSincePrevTick = ref(0);
 const active = ref(0);
diff --git a/packages/frontend/src/pages/settings/preferences-backups.vue b/packages/frontend/src/pages/settings/preferences-backups.vue
index 6613ce4c1d8639758668f1276e4d48c1c11c001c..86f633c445c1a3d93c7294043e2f28e56a48ce01 100644
--- a/packages/frontend/src/pages/settings/preferences-backups.vue
+++ b/packages/frontend/src/pages/settings/preferences-backups.vue
@@ -40,7 +40,7 @@ import MkInfo from '@/components/MkInfo.vue';
 import * as os from '@/os';
 import { ColdDeviceStorage, defaultStore } from '@/store';
 import { unisonReload } from '@/scripts/unison-reload';
-import { stream } from '@/stream';
+import { useStream } from '@/stream';
 import { $i } from '@/account';
 import { i18n } from '@/i18n';
 import { version, host } from '@/config';
@@ -125,7 +125,7 @@ type Profile = {
 	};
 };
 
-const connection = $i && stream.useChannel('main');
+const connection = $i && useStream().useChannel('main');
 
 let profiles = $ref<Record<string, Profile> | null>(null);
 
diff --git a/packages/frontend/src/pizzax.ts b/packages/frontend/src/pizzax.ts
index 2616a8a1d5f1d26206eb90f176c9bf789e135e5f..d97bd4be62e9a996c077b06e150b1d393faa5e1b 100644
--- a/packages/frontend/src/pizzax.ts
+++ b/packages/frontend/src/pizzax.ts
@@ -6,7 +6,7 @@ import { $i } from './account';
 import { api } from './os';
 import { get, set } from './scripts/idb-proxy';
 import { defaultStore } from './store';
-import { stream } from './stream';
+import { useStream } from './stream';
 import { deepClone } from './scripts/clone';
 
 type StateDef = Record<string, {
@@ -26,8 +26,6 @@ type PizzaxChannelMessage<T extends StateDef> = {
 	userId?: string;
 };
 
-const connection = $i && stream.useChannel('main');
-
 export class Storage<T extends StateDef> {
 	public readonly ready: Promise<void>;
 	public readonly loaded: Promise<void>;
@@ -105,8 +103,10 @@ export class Storage<T extends StateDef> {
 		});
 
 		if ($i) {
+			const connection = useStream().useChannel('main');
+
 			// streamingのuser storage updateイベントを監視して更新
-			connection?.on('registryUpdated', ({ scope, key, value }: { scope?: string[], key: keyof T, value: T[typeof key]['default'] }) => {
+			connection.on('registryUpdated', ({ scope, key, value }: { scope?: string[], key: keyof T, value: T[typeof key]['default'] }) => {
 				if (!scope || scope.length !== 2 || scope[0] !== 'client' || scope[1] !== this.key || this.state[key] === value) return;
 
 				this.reactiveState[key].value = this.state[key] = value;
diff --git a/packages/frontend/src/scripts/select-file.ts b/packages/frontend/src/scripts/select-file.ts
index fe9f0a2447ae22626a81b72c4f9fd18b50d1beb2..44a58d6c7d61f36fa1e8093206e298130374ddc5 100644
--- a/packages/frontend/src/scripts/select-file.ts
+++ b/packages/frontend/src/scripts/select-file.ts
@@ -1,7 +1,7 @@
 import { ref } from 'vue';
 import { DriveFile } from 'misskey-js/built/entities';
 import * as os from '@/os';
-import { stream } from '@/stream';
+import { useStream } from '@/stream';
 import { i18n } from '@/i18n';
 import { defaultStore } from '@/store';
 import { uploadFile } from '@/scripts/upload';
@@ -51,7 +51,7 @@ export function chooseFileFromUrl(): Promise<DriveFile> {
 
 			const marker = Math.random().toString(); // TODO: UUIDとか使う
 
-			const connection = stream.useChannel('main');
+			const connection = useStream().useChannel('main');
 			connection.on('urlUploadFinished', urlResponse => {
 				if (urlResponse.marker === marker) {
 					res(urlResponse.file);
diff --git a/packages/frontend/src/scripts/use-note-capture.ts b/packages/frontend/src/scripts/use-note-capture.ts
index ffe33cccc09c1def1789ea04efd98b8494ac6c54..22a01e066a347e92cce8e3e5eb7ed7e0e008bf4f 100644
--- a/packages/frontend/src/scripts/use-note-capture.ts
+++ b/packages/frontend/src/scripts/use-note-capture.ts
@@ -1,6 +1,6 @@
 import { onUnmounted, Ref } from 'vue';
 import * as misskey from 'misskey-js';
-import { stream } from '@/stream';
+import { useStream } from '@/stream';
 import { $i } from '@/account';
 
 export function useNoteCapture(props: {
@@ -9,7 +9,7 @@ export function useNoteCapture(props: {
 	isDeletedRef: Ref<boolean>;
 }) {
 	const note = props.note;
-	const connection = $i ? stream : null;
+	const connection = $i ? useStream() : null;
 
 	function onStreamNoteUpdated(noteData): void {
 		const { type, id, body } = noteData;
diff --git a/packages/frontend/src/stream.ts b/packages/frontend/src/stream.ts
index dea3459b862334a79a0664cb565f74be943ff113..9cae58a26a42d8241e8f2f5c19284a31cf3e633c 100644
--- a/packages/frontend/src/stream.ts
+++ b/packages/frontend/src/stream.ts
@@ -3,6 +3,14 @@ import { markRaw } from 'vue';
 import { $i } from '@/account';
 import { url } from '@/config';
 
-export const stream = markRaw(new Misskey.Stream(url, $i ? {
-	token: $i.token,
-} : null));
+let stream: Misskey.Stream | null = null;
+
+export function useStream(): Misskey.Stream {
+	if (stream) return stream;
+
+	stream = markRaw(new Misskey.Stream(url, $i ? {
+		token: $i.token,
+	} : null));
+
+	return stream;
+}
diff --git a/packages/frontend/src/ui/_common_/common.vue b/packages/frontend/src/ui/_common_/common.vue
index 71a4285e9db12c95c580258c0ad4bb3849b6981b..2beebb03905ea494f99cb974dd3b3ac553d899e6 100644
--- a/packages/frontend/src/ui/_common_/common.vue
+++ b/packages/frontend/src/ui/_common_/common.vue
@@ -40,7 +40,7 @@ import { popups, pendingApiRequestsCount } from '@/os';
 import { uploads } from '@/scripts/upload';
 import * as sound from '@/scripts/sound';
 import { $i } from '@/account';
-import { stream } from '@/stream';
+import { useStream } from '@/stream';
 import { i18n } from '@/i18n';
 import { defaultStore } from '@/store';
 
@@ -55,7 +55,7 @@ function onNotification(notification) {
 	if ($i.mutingNotificationTypes.includes(notification.type)) return;
 
 	if (document.visibilityState === 'visible') {
-		stream.send('readNotification');
+		useStream().send('readNotification');
 
 		notifications.unshift(notification);
 		window.setTimeout(() => {
@@ -71,7 +71,7 @@ function onNotification(notification) {
 }
 
 if ($i) {
-	const connection = stream.useChannel('main', null, 'UI');
+	const connection = useStream().useChannel('main', null, 'UI');
 	connection.on('notification', onNotification);
 
 	//#region Listen message from SW
diff --git a/packages/frontend/src/ui/_common_/stream-indicator.vue b/packages/frontend/src/ui/_common_/stream-indicator.vue
index 2a856e2a4542d2f9494b279605fc8e1be94538ad..3e97df7ee583c2f1207919a0877e718c166f4343 100644
--- a/packages/frontend/src/ui/_common_/stream-indicator.vue
+++ b/packages/frontend/src/ui/_common_/stream-indicator.vue
@@ -10,7 +10,7 @@
 
 <script lang="ts" setup>
 import { onUnmounted } from 'vue';
-import { stream } from '@/stream';
+import { useStream } from '@/stream';
 import { i18n } from '@/i18n';
 import MkButton from '@/components/MkButton.vue';
 import * as os from '@/os';
@@ -32,10 +32,10 @@ function reload() {
 	location.reload();
 }
 
-stream.on('_disconnected_', onDisconnected);
+useStream().on('_disconnected_', onDisconnected);
 
 onUnmounted(() => {
-	stream.off('_disconnected_', onDisconnected);
+	useStream().off('_disconnected_', onDisconnected);
 });
 </script>
 
diff --git a/packages/frontend/src/ui/minimum.vue b/packages/frontend/src/ui/minimum.vue
new file mode 100644
index 0000000000000000000000000000000000000000..628390e3f7bd2750c9d144a63bcdc243595c8aa8
--- /dev/null
+++ b/packages/frontend/src/ui/minimum.vue
@@ -0,0 +1,34 @@
+<template>
+<div class="mk-app" style="container-type: inline-size;">
+	<RouterView/>
+
+	<XCommon/>
+</div>
+</template>
+
+<script lang="ts" setup>
+import { provide, ComputedRef } from 'vue';
+import XCommon from './_common_/common.vue';
+import { mainRouter } from '@/router';
+import { PageMetadata, provideMetadataReceiver } from '@/scripts/page-metadata';
+import { instanceName } from '@/config';
+
+let pageMetadata = $ref<null | ComputedRef<PageMetadata>>();
+
+provide('router', mainRouter);
+provideMetadataReceiver((info) => {
+	pageMetadata = info;
+	if (pageMetadata.value) {
+		document.title = `${pageMetadata.value.title} | ${instanceName}`;
+	}
+});
+
+document.documentElement.style.overflowY = 'scroll';
+</script>
+
+<style lang="scss" scoped>
+.mk-app {
+	min-height: 100dvh;
+	box-sizing: border-box;
+}
+</style>
diff --git a/packages/frontend/src/widgets/WidgetJobQueue.vue b/packages/frontend/src/widgets/WidgetJobQueue.vue
index 84043cf13fb24f9932e2669215c52a1df1bab4b8..73a48025952dd60eb52ce1fb899bd29639cd5bbd 100644
--- a/packages/frontend/src/widgets/WidgetJobQueue.vue
+++ b/packages/frontend/src/widgets/WidgetJobQueue.vue
@@ -49,7 +49,7 @@
 import { onUnmounted, reactive } from 'vue';
 import { useWidgetPropsManager, Widget, WidgetComponentExpose } from './widget';
 import { GetFormResultType } from '@/scripts/form';
-import { stream } from '@/stream';
+import { useStream } from '@/stream';
 import number from '@/filters/number';
 import * as sound from '@/scripts/sound';
 import { deepClone } from '@/scripts/clone';
@@ -81,7 +81,7 @@ const { widgetProps, configure } = useWidgetPropsManager(name,
 	emit,
 );
 
-const connection = stream.useChannel('queueStats');
+const connection = useStream().useChannel('queueStats');
 const current = reactive({
 	inbox: {
 		activeSincePrevTick: 0,
diff --git a/packages/frontend/src/widgets/WidgetPhotos.vue b/packages/frontend/src/widgets/WidgetPhotos.vue
index 716bbb42749139c475bd9996a8384c91db0276e2..1484aa19419a7c6f72928a75c9e59cabab15c195 100644
--- a/packages/frontend/src/widgets/WidgetPhotos.vue
+++ b/packages/frontend/src/widgets/WidgetPhotos.vue
@@ -20,7 +20,7 @@
 import { onUnmounted, ref } from 'vue';
 import { useWidgetPropsManager, Widget, WidgetComponentExpose } from './widget';
 import { GetFormResultType } from '@/scripts/form';
-import { stream } from '@/stream';
+import { useStream } from '@/stream';
 import { getStaticImageUrl } from '@/scripts/media-proxy';
 import * as os from '@/os';
 import MkContainer from '@/components/MkContainer.vue';
@@ -54,7 +54,7 @@ const { widgetProps, configure } = useWidgetPropsManager(name,
 	emit,
 );
 
-const connection = stream.useChannel('main');
+const connection = useStream().useChannel('main');
 const images = ref([]);
 const fetching = ref(true);
 
diff --git a/packages/frontend/src/widgets/server-metric/index.vue b/packages/frontend/src/widgets/server-metric/index.vue
index 357d0ab78b3757e5d11d8c068a41c12fa07ace51..df8de10f63cf191eeb4f0f046a839e60e4076efb 100644
--- a/packages/frontend/src/widgets/server-metric/index.vue
+++ b/packages/frontend/src/widgets/server-metric/index.vue
@@ -25,7 +25,7 @@ import XDisk from './disk.vue';
 import MkContainer from '@/components/MkContainer.vue';
 import { GetFormResultType } from '@/scripts/form';
 import * as os from '@/os';
-import { stream } from '@/stream';
+import { useStream } from '@/stream';
 import { i18n } from '@/i18n';
 
 const name = 'serverMetric';
@@ -75,7 +75,7 @@ const toggleView = () => {
 	save();
 };
 
-const connection = stream.useChannel('serverStats');
+const connection = useStream().useChannel('serverStats');
 onUnmounted(() => {
 	connection.dispose();
 });
diff --git a/packages/frontend/vite.config.ts b/packages/frontend/vite.config.ts
index fad0dd0177a48088b8ec1562cd8afe636a751edb..339761e78cb39edd8b03c7ebbf005673f3b1986f 100644
--- a/packages/frontend/vite.config.ts
+++ b/packages/frontend/vite.config.ts
@@ -117,7 +117,7 @@ export function getConfig(): UserConfig {
 			manifest: 'manifest.json',
 			rollupOptions: {
 				input: {
-					app: './src/init.ts',
+					app: './src/_boot_.ts',
 				},
 				output: {
 					manualChunks: {