diff --git a/packages/frontend/src/components/MkMenu.vue b/packages/frontend/src/components/MkMenu.vue
index 52aba58455..09d530c4ea 100644
--- a/packages/frontend/src/components/MkMenu.vue
+++ b/packages/frontend/src/components/MkMenu.vue
@@ -56,7 +56,7 @@
 </template>
 
 <script lang="ts" setup>
-import { defineAsyncComponent, nextTick, onBeforeUnmount, onMounted, watch } from 'vue';
+import { defineAsyncComponent, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue';
 import { focusPrev, focusNext } from '@/scripts/focus';
 import MkSwitch from '@/components/MkSwitch.vue';
 import { MenuItem, InnerMenuItem, MenuPending, MenuAction } from '@/types/menu';
@@ -111,11 +111,11 @@ watch(() => props.items, () => {
 	immediate: true,
 });
 
-let childMenu = $ref<MenuItem[] | null>();
+let childMenu = ref<MenuItem[] | null>();
 let childTarget = $shallowRef<HTMLElement | null>();
 
 function closeChild() {
-	childMenu = null;
+	childMenu.value = null;
 	childShowingItem = null;
 }
 
@@ -140,13 +140,31 @@ function onItemMouseLeave(item) {
 	if (childCloseTimer) window.clearTimeout(childCloseTimer);
 }
 
+let childrenCache = new WeakMap();
 async function showChildren(item: MenuItem, ev: MouseEvent) {
+	const children = ref([]);
+	if (childrenCache.has(item)) {
+		children.value = childrenCache.get(item);
+	} else {
+		if (typeof item.children === 'function') {
+			children.value = [{
+				type: 'pending',
+			}];
+			item.children().then(x => {
+				children.value = x;
+				childrenCache.set(item, x);
+			});
+		} else {
+			children.value = item.children;
+		}
+	}
+
 	if (props.asDrawer) {
-		os.popupMenu(item.children, ev.currentTarget ?? ev.target);
+		os.popupMenu(children, ev.currentTarget ?? ev.target);
 		close();
 	} else {
 		childTarget = ev.currentTarget ?? ev.target;
-		childMenu = item.children;
+		childMenu = children;
 		childShowingItem = item;
 	}
 }
diff --git a/packages/frontend/src/scripts/get-note-menu.ts b/packages/frontend/src/scripts/get-note-menu.ts
index 9da7447bfd..f732c259fb 100644
--- a/packages/frontend/src/scripts/get-note-menu.ts
+++ b/packages/frontend/src/scripts/get-note-menu.ts
@@ -99,66 +99,6 @@ export function getNoteMenu(props: {
 		});
 	}
 
-	async function clip(): Promise<void> {
-		const clips = await os.api('clips/list');
-		os.popupMenu([{
-			icon: 'ti ti-plus',
-			text: i18n.ts.createNew,
-			action: async () => {
-				const { canceled, result } = await os.form(i18n.ts.createNewClip, {
-					name: {
-						type: 'string',
-						label: i18n.ts.name,
-					},
-					description: {
-						type: 'string',
-						required: false,
-						multiline: true,
-						label: i18n.ts.description,
-					},
-					isPublic: {
-						type: 'boolean',
-						label: i18n.ts.public,
-						default: false,
-					},
-				});
-				if (canceled) return;
-
-				const clip = await os.apiWithDialog('clips/create', result);
-
-				claimAchievement('noteClipped1');
-				os.apiWithDialog('clips/add-note', { clipId: clip.id, noteId: appearNote.id });
-			},
-		}, null, ...clips.map(clip => ({
-			text: clip.name,
-			action: () => {
-				claimAchievement('noteClipped1');
-				os.promiseDialog(
-					os.api('clips/add-note', { clipId: clip.id, noteId: appearNote.id }),
-					null,
-					async (err) => {
-						if (err.id === '734806c4-542c-463a-9311-15c512803965') {
-							const confirm = await os.confirm({
-								type: 'warning',
-								text: i18n.t('confirmToUnclipAlreadyClippedNote', { name: clip.name }),
-							});
-							if (!confirm.canceled) {
-								os.apiWithDialog('clips/remove-note', { clipId: clip.id, noteId: appearNote.id });
-								if (props.currentClipPage?.value.id === clip.id) props.isDeleted.value = true;
-							}
-						} else {
-							os.alert({
-								type: 'error',
-								text: err.message + '\n' + err.id,
-							});
-						}
-					},
-				);
-			},
-		}))], props.menuButton.value, {
-		}).then(focus);
-	}
-
 	async function unclip(): Promise<void> {
 		os.apiWithDialog('clips/remove-note', { clipId: props.currentClipPage.value.id, noteId: appearNote.id });
 		props.isDeleted.value = true;
@@ -264,9 +204,67 @@ export function getNoteMenu(props: {
 				action: () => toggleFavorite(true),
 			}),
 			{
+				type: 'parent',
 				icon: 'ti ti-paperclip',
 				text: i18n.ts.clip,
-				action: () => clip(),
+				children: async () => {
+					const clips = await os.api('clips/list');
+					return [{
+						icon: 'ti ti-plus',
+						text: i18n.ts.createNew,
+						action: async () => {
+							const { canceled, result } = await os.form(i18n.ts.createNewClip, {
+								name: {
+									type: 'string',
+									label: i18n.ts.name,
+								},
+								description: {
+									type: 'string',
+									required: false,
+									multiline: true,
+									label: i18n.ts.description,
+								},
+								isPublic: {
+									type: 'boolean',
+									label: i18n.ts.public,
+									default: false,
+								},
+							});
+							if (canceled) return;
+
+							const clip = await os.apiWithDialog('clips/create', result);
+
+							claimAchievement('noteClipped1');
+							os.apiWithDialog('clips/add-note', { clipId: clip.id, noteId: appearNote.id });
+						},
+					}, null, ...clips.map(clip => ({
+						text: clip.name,
+						action: () => {
+							claimAchievement('noteClipped1');
+							os.promiseDialog(
+								os.api('clips/add-note', { clipId: clip.id, noteId: appearNote.id }),
+								null,
+								async (err) => {
+									if (err.id === '734806c4-542c-463a-9311-15c512803965') {
+										const confirm = await os.confirm({
+											type: 'warning',
+											text: i18n.t('confirmToUnclipAlreadyClippedNote', { name: clip.name }),
+										});
+										if (!confirm.canceled) {
+											os.apiWithDialog('clips/remove-note', { clipId: clip.id, noteId: appearNote.id });
+											if (props.currentClipPage?.value.id === clip.id) props.isDeleted.value = true;
+										}
+									} else {
+										os.alert({
+											type: 'error',
+											text: err.message + '\n' + err.id,
+										});
+									}
+								},
+							);
+						},
+					}))];
+				},
 			},
 			statePromise.then(state => state.isMutedThread ? {
 				icon: 'ti ti-message-off',