diff --git a/packages/frontend/src/components/global/MkLazy.vue b/packages/frontend/src/components/global/MkLazy.vue
new file mode 100644
index 000000000..6d7ff4ca4
--- /dev/null
+++ b/packages/frontend/src/components/global/MkLazy.vue
@@ -0,0 +1,53 @@
+<!--
+SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
+<template>
+<div ref="rootEl" :class="$style.root">
+	<div v-if="!showing" :class="$style.placeholder"></div>
+	<slot v-else></slot>
+</div>
+</template>
+
+<script lang="ts" setup>
+import { nextTick, onMounted, onActivated, onBeforeUnmount, ref, shallowRef } from 'vue';
+
+const rootEl = shallowRef<HTMLDivElement>();
+const showing = ref(false);
+
+const observer = new IntersectionObserver(
+	(entries) => {
+		if (entries.some((entry) => entry.isIntersecting)) {
+			showing.value = true;
+		}
+	},
+);
+
+onMounted(() => {
+	nextTick(() => {
+		observer.observe(rootEl.value!);
+	});
+});
+
+onActivated(() => {
+	nextTick(() => {
+		observer.observe(rootEl.value!);
+	});
+});
+
+onBeforeUnmount(() => {
+	observer.disconnect();
+});
+</script>
+
+<style lang="scss" module>
+.root {
+	display: block;
+}
+
+.placeholder {
+	display: block;
+	min-height: 150px;
+}
+</style>
diff --git a/packages/frontend/src/components/index.ts b/packages/frontend/src/components/index.ts
index c740d181f..a3e13c3a5 100644
--- a/packages/frontend/src/components/index.ts
+++ b/packages/frontend/src/components/index.ts
@@ -25,6 +25,7 @@ import MkPageHeader from './global/MkPageHeader.vue';
 import MkSpacer from './global/MkSpacer.vue';
 import MkFooterSpacer from './global/MkFooterSpacer.vue';
 import MkStickyContainer from './global/MkStickyContainer.vue';
+import MkLazy from './global/MkLazy.vue';
 
 export default function(app: App) {
 	for (const [key, value] of Object.entries(components)) {
@@ -53,6 +54,7 @@ export const components = {
 	MkSpacer: MkSpacer,
 	MkFooterSpacer: MkFooterSpacer,
 	MkStickyContainer: MkStickyContainer,
+	MkLazy: MkLazy,
 };
 
 declare module '@vue/runtime-core' {
@@ -77,5 +79,6 @@ declare module '@vue/runtime-core' {
 		MkSpacer: typeof MkSpacer;
 		MkFooterSpacer: typeof MkFooterSpacer;
 		MkStickyContainer: typeof MkStickyContainer;
+		MkLazy: typeof MkLazy;
 	}
 }
diff --git a/packages/frontend/src/pages/user/home.vue b/packages/frontend/src/pages/user/home.vue
index ecc3bb36c..a0163bfc6 100644
--- a/packages/frontend/src/pages/user/home.vue
+++ b/packages/frontend/src/pages/user/home.vue
@@ -128,12 +128,18 @@ SPDX-License-Identifier: AGPL-3.0-only
 				</div>
 				<MkInfo v-else-if="$i && $i.id === user.id">{{ i18n.ts.userPagePinTip }}</MkInfo>
 				<template v-if="narrow">
-					<XFiles :key="user.id" :user="user"/>
-					<XActivity :key="user.id" :user="user"/>
+					<MkLazy>
+						<XFiles :key="user.id" :user="user"/>
+					</MkLazy>
+					<MkLazy>
+						<XActivity :key="user.id" :user="user"/>
+					</MkLazy>
 				</template>
 				<div v-if="!disableNotes">
 					<div style="margin-bottom: 8px;">{{ i18n.ts.featured }}</div>
-					<MkNotes :class="$style.tl" :noGap="true" :pagination="pagination"/>
+					<MkLazy>
+						<MkNotes :class="$style.tl" :noGap="true" :pagination="pagination"/>
+					</MkLazy>
 				</div>
 			</div>
 		</div>