diff --git a/packages/frontend/src/components/MkReactionsViewer.details.vue b/packages/frontend/src/components/MkReactionsViewer.details.vue
index d24e0b15bf..f41a8a40f3 100644
--- a/packages/frontend/src/components/MkReactionsViewer.details.vue
+++ b/packages/frontend/src/components/MkReactionsViewer.details.vue
@@ -100,4 +100,4 @@ function getReactionName(reaction: string): string {
 .more {
 	padding-top: 4px;
 }
-</style>
+</style>
\ No newline at end of file
diff --git a/packages/frontend/src/components/MkReactionsViewer.reaction.vue b/packages/frontend/src/components/MkReactionsViewer.reaction.vue
index 56bbf1b0b5..f35c938d02 100644
--- a/packages/frontend/src/components/MkReactionsViewer.reaction.vue
+++ b/packages/frontend/src/components/MkReactionsViewer.reaction.vue
@@ -18,7 +18,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { computed, inject, onBeforeMount, shallowRef, watch } from 'vue';
+import { computed, inject, onMounted, shallowRef, watch } from 'vue';
 import * as Misskey from 'misskey-js';
 import { getUnicodeEmoji } from '@@/js/emojilist.js';
 import MkCustomEmojiDetailedDialog from './MkCustomEmojiDetailedDialog.vue';
@@ -32,37 +32,10 @@ import MkReactionEffect from '@/components/MkReactionEffect.vue';
 import { claimAchievement } from '@/utility/achievements.js';
 import { i18n } from '@/i18n.js';
 import * as sound from '@/utility/sound.js';
+import { checkReactionPermissions } from '@/utility/check-reaction-permissions.js';
 import { customEmojisMap } from '@/custom-emojis.js';
 import { prefer } from '@/preferences.js';
 
-const localEmojiSet = new Set(Array.from(customEmojisMap.keys()));
-const reactionCache = new Map<string, { hasNative: boolean; base: string }>();
-
-function getReactionInfo(reaction: string) {
-	if (reactionCache.has(reaction)) {
-		const cachedReaction = reactionCache.get(reaction);
-		if (cachedReaction) {
-			return cachedReaction;
-		}
-	}
-
-	let hasNative: boolean;
-	let base: string;
-
-	if (!reaction.includes(':')) {
-		hasNative = true;
-		base = reaction;
-	} else {
-		const baseName = reaction.split('@')[0].split(':')[1];
-		hasNative = localEmojiSet.has(baseName);
-		base = hasNative ? `:${baseName}:` : reaction;
-	}
-
-	const info = { hasNative, base };
-	reactionCache.set(reaction, info);
-	return info;
-}
-
 const props = defineProps<{
 	reaction: string;
 	count: number;
@@ -81,39 +54,40 @@ const buttonEl = shallowRef<HTMLElement>();
 const emojiName = computed(() => props.reaction.replace(/:/g, '').replace(/@\./, ''));
 const emoji = computed(() => customEmojisMap.get(emojiName.value) ?? getUnicodeEmoji(props.reaction));
 
-const reactionInfo = computed(() => getReactionInfo(props.reaction));
-const hasNativeEmoji = computed(() => reactionInfo.value.hasNative);
-const baseReaction = computed(() => reactionInfo.value.base);
-
-const canToggle = computed(() => $i != null && hasNativeEmoji.value);
-
-const isReacted = computed(() => {
-	if (!props.note.myReaction) return false;
-	const myInfo = getReactionInfo(props.note.myReaction);
-	return myInfo.base === reactionInfo.value.base;
+const canToggle = computed(() => {
+	return !props.reaction.match(/@\w/) && $i && emoji.value && checkReactionPermissions($i, props.note, emoji.value);
 });
-
-let lastCount = props.count;
+const canGetInfo = computed(() => !props.reaction.match(/@\w/) && props.reaction.includes(':'));
 
 async function toggleReaction() {
 	if (!canToggle.value) return;
 
 	const oldReaction = props.note.myReaction;
-
-	if (isReacted.value) {
+	if (oldReaction) {
 		const confirm = await os.confirm({
 			type: 'warning',
-			text: i18n.ts.cancelReactionConfirm,
+			text: oldReaction !== props.reaction ? i18n.ts.changeReactionConfirm : i18n.ts.cancelReactionConfirm,
 		});
 		if (confirm.canceled) return;
 
+		if (oldReaction !== props.reaction) {
+			sound.playMisskeySfx('reaction');
+		}
+
 		if (mock) {
 			emit('reactionToggled', props.reaction, (props.count - 1));
 			return;
 		}
 
-		await misskeyApi('notes/reactions/delete', {
+		misskeyApi('notes/reactions/delete', {
 			noteId: props.note.id,
+		}).then(() => {
+			if (oldReaction !== props.reaction) {
+				misskeyApi('notes/reactions/create', {
+					noteId: props.note.id,
+					reaction: props.reaction,
+				});
+			}
 		});
 	} else {
 		if (prefer.s.confirmOnReact) {
@@ -121,28 +95,10 @@ async function toggleReaction() {
 				type: 'question',
 				text: i18n.tsx.reactAreYouSure({ emoji: props.reaction.replace('@.', '') }),
 			});
+
 			if (confirm.canceled) return;
 		}
 
-		if (oldReaction) {
-			const confirm = await os.confirm({
-				type: 'warning',
-				text: i18n.ts.changeReactionConfirm,
-			});
-			if (confirm.canceled) return;
-
-			sound.playMisskeySfx('reaction');
-
-			if (mock) {
-				emit('reactionToggled', props.reaction, (props.count + 1));
-				return;
-			}
-
-			await misskeyApi('notes/reactions/delete', {
-				noteId: props.note.id,
-			});
-		}
-
 		sound.playMisskeySfx('reaction');
 
 		if (mock) {
@@ -152,9 +108,8 @@ async function toggleReaction() {
 
 		misskeyApi('notes/reactions/create', {
 			noteId: props.note.id,
-			reaction: baseReaction.value,
+			reaction: props.reaction,
 		});
-
 		if (props.note.text && props.note.text.length > 100 && (Date.now() - new Date(props.note.createdAt).getTime() < 1000 * 3)) {
 			claimAchievement('reactWithoutRead');
 		}
@@ -162,7 +117,7 @@ async function toggleReaction() {
 }
 
 async function menu(ev) {
-	if (!props.reaction.includes(':')) return;
+	if (!canGetInfo.value) return;
 
 	os.popupMenu([{
 		text: i18n.ts.info,
@@ -191,54 +146,28 @@ function anime() {
 }
 
 watch(() => props.count, (newCount, oldCount) => {
-	if (oldCount < newCount && !props.isInitial) anime();
-	lastCount = newCount;
-}, { immediate: true });
+	if (oldCount < newCount) anime();
+});
 
-onBeforeMount(() => {
-	getReactionInfo(props.reaction);
-	if (props.note.myReaction) {
-		getReactionInfo(props.note.myReaction);
-	}
-	Object.keys(props.note.reactions).forEach(reaction => {
-		getReactionInfo(reaction);
-	});
+onMounted(() => {
+	if (!props.isInitial) anime();
 });
 
 if (!mock) {
 	useTooltip(buttonEl, async (showing) => {
-		const allVariants = new Set([props.reaction]);
+		const reactions = await misskeyApiGet('notes/reactions', {
+			noteId: props.note.id,
+			type: props.reaction,
+			limit: 10,
+			_cacheKey_: props.count,
+		});
 
-		if (reactionInfo.value.hasNative) {
-			allVariants.add(reactionInfo.value.base);
-
-			Object.keys(props.note.reactions).forEach(reaction => {
-				const info = getReactionInfo(reaction);
-				if (info.hasNative && info.base === reactionInfo.value.base) {
-					allVariants.add(reaction);
-				}
-			});
-		}
-
-		const reactionPromises = Array.from(allVariants).map(variant =>
-			misskeyApiGet('notes/reactions', {
-				noteId: props.note.id,
-				type: variant,
-				limit: 10,
-				_cacheKey_: props.count,
-			}),
-		);
-
-		const allReactions = await Promise.all(reactionPromises);
-
-		const allUsers = [...new Map(
-			allReactions.flat().map(x => [x.user.id, x.user]),
-		).values()];
+		const users = reactions.map(x => x.user);
 
 		const { dispose } = os.popup(XDetails, {
 			showing,
 			reaction: props.reaction,
-			users: allUsers,
+			users,
 			count: props.count,
 			targetElement: buttonEl.value,
 		}, {
diff --git a/packages/frontend/src/components/MkReactionsViewer.vue b/packages/frontend/src/components/MkReactionsViewer.vue
index c3266c13e3..8acbdd5a05 100644
--- a/packages/frontend/src/components/MkReactionsViewer.vue
+++ b/packages/frontend/src/components/MkReactionsViewer.vue
@@ -12,43 +12,16 @@ SPDX-License-Identifier: AGPL-3.0-only
 	:moveClass="prefer.s.animation ? $style.transition_x_move : ''"
 	tag="div" :class="$style.root"
 >
-	<XReaction v-for="[reaction, count] in mergedReactions" :key="reaction" :reaction="reaction" :count="count" :isInitial="initialReactions.has(reaction)" :note="note" @reactionToggled="onMockToggleReaction"/>
+	<XReaction v-for="[reaction, count] in reactions" :key="reaction" :reaction="reaction" :count="count" :isInitial="initialReactions.has(reaction)" :note="note" @reactionToggled="onMockToggleReaction"/>
 	<slot v-if="hasMoreReactions" name="more"/>
 </TransitionGroup>
 </template>
 
 <script lang="ts" setup>
 import * as Misskey from 'misskey-js';
-import { inject, watch, ref, computed, onBeforeMount } from 'vue';
+import { inject, watch, ref } from 'vue';
 import XReaction from '@/components/MkReactionsViewer.reaction.vue';
 import { prefer } from '@/preferences.js';
-import { customEmojisMap } from '@/custom-emojis.js';
-
-const localEmojiSet = new Set(Array.from(customEmojisMap.keys()));
-const emojiCache = new Map<string, boolean>();
-
-function hasLocalEmoji(reaction: string): boolean {
-	if (emojiCache.has(reaction)) {
-		const cachedResult = emojiCache.get(reaction);
-		if (cachedResult !== undefined) return cachedResult;
-	}
-
-	let result: boolean;
-	if (!reaction.includes(':')) {
-		result = true;
-	} else {
-		const emojiName = reaction.split('@')[0].split(':')[1];
-		result = localEmojiSet.has(emojiName);
-	}
-
-	emojiCache.set(reaction, result);
-	return result;
-}
-
-function getBaseReaction(reaction: string): string {
-	if (!reaction.includes(':')) return reaction;
-	return `:${reaction.split('@')[0].split(':')[1]}:`;
-}
 
 const props = withDefaults(defineProps<{
 	note: Misskey.entities.Note;
@@ -63,44 +36,16 @@ const emit = defineEmits<{
 	(ev: 'mockUpdateMyReaction', emoji: string, delta: number): void;
 }>();
 
-const initialReactions = new Set(Object.keys(props.note.reactions));
+const initialReactions = ref(new Set<string>());
 
 const reactions = ref<[string, number][]>([]);
 const hasMoreReactions = ref(false);
 
-const mergedReactions = computed(() => {
-	const reactionMap = new Map();
-
-	reactions.value.forEach(([reaction, count]) => {
-		if (!hasLocalEmoji(reaction)) {
-			if (reactionMap.has(reaction)) {
-				reactionMap.set(reaction, reactionMap.get(reaction) + count);
-			} else {
-				reactionMap.set(reaction, count);
-			}
-			return;
-		}
-
-		const baseReaction = getBaseReaction(reaction);
-		if (reactionMap.has(baseReaction)) {
-			reactionMap.set(baseReaction, reactionMap.get(baseReaction) + count);
-		} else {
-			reactionMap.set(baseReaction, count);
-		}
-	});
-
-	return Array.from(reactionMap.entries());
-});
-
-if (props.note.myReaction && !Object.keys(reactions.value).includes(props.note.myReaction)) {
-	reactions.value[props.note.myReaction] = props.note.reactions[props.note.myReaction];
-}
-
-onBeforeMount(() => {
-	Object.keys(props.note.reactions).forEach(reaction => {
-		hasLocalEmoji(reaction);
-	});
-});
+watch(() => props.note.myReaction, (newMyReaction) => {
+	if (newMyReaction && !Object.keys(reactions.value).includes(newMyReaction)) {
+		reactions.value[newMyReaction] = props.note.reactions[newMyReaction];
+	}
+}, { immediate: true });
 
 function onMockToggleReaction(emoji: string, count: number) {
 	if (!mock) return;
@@ -112,6 +57,7 @@ function onMockToggleReaction(emoji: string, count: number) {
 }
 
 watch([() => props.note.reactions, () => props.maxNumber], ([newSource, maxNumber]) => {
+	initialReactions.value = new Set(Object.keys(newSource));
 	let newReactions: [string, number][] = [];
 	hasMoreReactions.value = Object.keys(newSource).length > maxNumber;
 
@@ -128,7 +74,7 @@ watch([() => props.note.reactions, () => props.maxNumber], ([newSource, maxNumbe
 		...newReactions,
 		...Object.entries(newSource)
 			.sort(([, a], [, b]) => b - a)
-			.filter(([y], i) => i < maxNumber && !newReactionsNames.includes(y)) as [string, number][],
+			.filter(([y], i) => i < maxNumber && !newReactionsNames.includes(y)),
 	];
 
 	newReactions = newReactions.slice(0, props.maxNumber);
@@ -166,4 +112,4 @@ watch([() => props.note.reactions, () => props.maxNumber], ([newSource, maxNumbe
 		display: none;
 	}
 }
-</style>
+</style>
\ No newline at end of file
diff --git a/packages/frontend/src/custom-emojis.ts b/packages/frontend/src/custom-emojis.ts
index c226b711a1..45d4b40fd7 100644
--- a/packages/frontend/src/custom-emojis.ts
+++ b/packages/frontend/src/custom-emojis.ts
@@ -8,128 +8,69 @@ import * as Misskey from 'misskey-js';
 import { misskeyApi, misskeyApiGet } from '@/utility/misskey-api.js';
 import { get, set } from '@/utility/idb-proxy.js';
 
-const CACHE_EXPIRE_TIME = 12 * 60 * 60 * 1000;
-const BATCH_SIZE = 1000;
-
 const storageCache = await get('emojis');
-export const customEmojis = shallowRef<Misskey.entities.EmojiSimple[]>(
-	Array.isArray(storageCache) ? storageCache : [],
-);
-
-const categoriesMap = new Map<string, Misskey.entities.EmojiSimple[]>();
-
-export const customEmojiCategories = computed<[...string[], null]>(() => {
-	if (categoriesMap.size === 0 && customEmojis.value.length > 0) {
-		for (const emoji of customEmojis.value) {
-			const category = emoji.category && emoji.category !== 'null'
-				? emoji.category
-				: 'null';
-
-			if (!categoriesMap.has(category)) {
-				categoriesMap.set(category, []);
-			}
-			categoriesMap.get(category)?.push(emoji);
+export const customEmojis = shallowRef<Misskey.entities.EmojiSimple[]>(Array.isArray(storageCache) ? storageCache : []);
+export const customEmojiCategories = computed<[ ...string[], null ]>(() => {
+	const categories = new Set<string>();
+	for (const emoji of customEmojis.value) {
+		if (emoji.category && emoji.category !== 'null') {
+			categories.add(emoji.category);
 		}
 	}
-	return markRaw([...Array.from(categoriesMap.keys()), null]);
+	return markRaw([...Array.from(categories), null]);
 });
 
 export const customEmojisMap = new Map<string, Misskey.entities.EmojiSimple>();
-
-function batchUpdateMap(emojis: Misskey.entities.EmojiSimple[]): void {
-	customEmojisMap.clear();
-	categoriesMap.clear();
-
-	for (let i = 0; i < emojis.length; i += BATCH_SIZE) {
-		const batch = emojis.slice(i, i + BATCH_SIZE);
-		for (const emoji of batch) {
-			customEmojisMap.set(emoji.name, emoji);
-		}
-	}
-}
-
 watch(customEmojis, emojis => {
-	batchUpdateMap(emojis);
+	customEmojisMap.clear();
+	for (const emoji of emojis) {
+		customEmojisMap.set(emoji.name, emoji);
+	}
 }, { immediate: true });
 
-export function addCustomEmoji(emoji: Misskey.entities.EmojiSimple): void {
-	const newEmojis = [emoji, ...customEmojis.value];
-	customEmojis.value = newEmojis;
-	customEmojisMap.set(emoji.name, emoji);
-	void set('emojis', newEmojis);
+export function addCustomEmoji(emoji: Misskey.entities.EmojiSimple) {
+	customEmojis.value = [emoji, ...customEmojis.value];
+	set('emojis', customEmojis.value);
 }
 
-export function updateCustomEmojis(emojis: Misskey.entities.EmojiSimple[]): void {
-	const updateMap = new Map(emojis.map(emoji => [emoji.name, emoji]));
-
-	const newEmojis = customEmojis.value.map(item =>
-		updateMap.get(item.name) ?? item,
-	);
-
-	customEmojis.value = newEmojis;
-	batchUpdateMap(newEmojis);
-	void set('emojis', newEmojis);
+export function updateCustomEmojis(emojis: Misskey.entities.EmojiSimple[]) {
+	customEmojis.value = customEmojis.value.map(item => emojis.find(search => search.name === item.name) ?? item);
+	set('emojis', customEmojis.value);
 }
 
-export function removeCustomEmojis(emojis: Misskey.entities.EmojiSimple[]): void {
-	const removedNames = new Set(emojis.map(e => e.name));
-
-	const filteredEmojis = customEmojis.value.filter(
-		item => !removedNames.has(item.name),
-	);
-
-	customEmojis.value = filteredEmojis;
-	batchUpdateMap(filteredEmojis);
-	void set('emojis', filteredEmojis);
+export function removeCustomEmojis(emojis: Misskey.entities.EmojiSimple[]) {
+	customEmojis.value = customEmojis.value.filter(item => !emojis.some(search => search.name === item.name));
+	set('emojis', customEmojis.value);
 }
 
-export async function fetchCustomEmojis(force = false): Promise<void> {
+export async function fetchCustomEmojis(force = false) {
 	const now = Date.now();
 
-	if (!force) {
+	let res;
+	if (force) {
+		res = await misskeyApi('emojis', {});
+	} else {
 		const lastFetchedAt = await get('lastEmojisFetchedAt');
-		if (lastFetchedAt && (now - lastFetchedAt) < CACHE_EXPIRE_TIME) {
-			return;
-		}
+		if (lastFetchedAt && (now - lastFetchedAt) < 1000 * 60 * 60) return;
+		res = await misskeyApiGet('emojis', {});
 	}
 
-	try {
-		const res = await (force ? misskeyApi : misskeyApiGet)('emojis', {});
-
-		if (res.emojis.length > 0) {
-			customEmojis.value = res.emojis;
-			await Promise.all([
-				set('emojis', res.emojis),
-				set('lastEmojisFetchedAt', now),
-			]);
-		}
-	} catch (error) {
-		console.error('Failed to fetch emojis:', error);
-	}
+	customEmojis.value = res.emojis;
+	set('emojis', res.emojis);
+	set('lastEmojisFetchedAt', now);
 }
 
-const tagsCache = new Map<string, string[]>();
+let cachedTags;
+export function getCustomEmojiTags() {
+	if (cachedTags) return cachedTags;
 
-export function getCustomEmojiTags(): string[] {
-	const cacheKey = `${customEmojis.value.length}`;
-
-	if (tagsCache.has(cacheKey)) {
-		const cached = tagsCache.get(cacheKey);
-		if (cached) return cached;
-	}
-
-	const tags = new Set<string>();
-
-	for (let i = 0; i < customEmojis.value.length; i += BATCH_SIZE) {
-		const batch = customEmojis.value.slice(i, i + BATCH_SIZE);
-		for (const emoji of batch) {
-			for (const tag of emoji.aliases) {
-				tags.add(tag);
-			}
+	const tags = new Set();
+	for (const emoji of customEmojis.value) {
+		for (const tag of emoji.aliases) {
+			tags.add(tag);
 		}
 	}
-
-	const result = Array.from(tags);
-	tagsCache.set(cacheKey, result);
-	return result;
+	const res = Array.from(tags);
+	cachedTags = res;
+	return res;
 }