diff --git a/src/client/app/common/define-widget.ts b/src/client/app/common/define-widget.ts
index 7b98c0903..0b2bc3656 100644
--- a/src/client/app/common/define-widget.ts
+++ b/src/client/app/common/define-widget.ts
@@ -18,61 +18,65 @@ export default function<T extends object>(data: {
 				default: false
 			}
 		},
+
 		computed: {
 			id(): string {
 				return this.widget.id;
+			},
+
+			props(): T {
+				return this.widget.data;
 			}
 		},
+
 		data() {
 			return {
-				props: data.props ? data.props() : {} as T,
-				bakedOldProps: null,
-				preventSave: false
+				bakedOldProps: null
 			};
 		},
+
 		created() {
-			if (this.props) {
-				Object.keys(this.props).forEach(prop => {
-					if (this.widget.data.hasOwnProperty(prop)) {
-						this.props[prop] = this.widget.data[prop];
-					}
-				});
-			}
+			this.mergeProps();
+
+			this.$watch('props', () => {
+				this.mergeProps();
+			});
 
 			this.bakeProps();
+		},
 
-			this.$watch('props', newProps => {
-				if (this.preventSave) {
-					this.preventSave = false;
-					this.bakeProps();
-					return;
+		methods: {
+			bakeProps() {
+				this.bakedOldProps = JSON.stringify(this.props);
+			},
+
+			mergeProps() {
+				if (data.props) {
+					const defaultProps = data.props();
+					Object.keys(defaultProps).forEach(prop => {
+						if (!this.props.hasOwnProperty(prop)) {
+							Vue.set(this.props, prop, defaultProps[prop]);
+						}
+					});
 				}
-				if (this.bakedOldProps == JSON.stringify(newProps)) return;
+			},
+
+			save() {
+				if (this.bakedOldProps == JSON.stringify(this.props)) return;
 
 				this.bakeProps();
 
 				if (this.isMobile) {
 					(this as any).api('i/update_mobile_home', {
 						id: this.id,
-						data: newProps
-					}).then(() => {
-						(this as any).os.i.clientSettings.mobileHome.find(w => w.id == this.id).data = newProps;
+						data: this.props
 					});
 				} else {
 					(this as any).api('i/update_home', {
 						id: this.id,
-						data: newProps
-					}).then(() => {
-						(this as any).os.i.clientSettings.home.find(w => w.id == this.id).data = newProps;
+						data: this.props
 					});
 				}
-			}, {
-				deep: true
-			});
-		},
-		methods: {
-			bakeProps() {
-				this.bakedOldProps = JSON.stringify(this.props);
 			}
 		}
 	});
diff --git a/src/client/app/common/mios.ts b/src/client/app/common/mios.ts
index 4e471cf96..7dcae4794 100644
--- a/src/client/app/common/mios.ts
+++ b/src/client/app/common/mios.ts
@@ -3,6 +3,7 @@ import { EventEmitter } from 'eventemitter3';
 import * as merge from 'object-assign-deep';
 import * as uuid from 'uuid';
 
+import initStore from '../store';
 import { hostname, apiUrl, swPublickey, version, lang, googleMapsApiKey } from '../config';
 import Progress from './scripts/loading';
 import Connection from './scripts/streaming/stream';
@@ -16,16 +17,6 @@ import Err from '../common/views/components/connect-failed.vue';
 import { LocalTimelineStreamManager } from './scripts/streaming/local-timeline';
 import { GlobalTimelineStreamManager } from './scripts/streaming/global-timeline';
 
-const defaultSettings = {
-	fetchOnScroll: true,
-	showMaps: true,
-	showPostFormOnTopOfTl: false,
-	gradientWindowHeader: false,
-	showReplyTarget: true,
-	showMyRenotes: true,
-	showRenotedMyNotes: true
-};
-
 //#region api requests
 let spinner = null;
 let pending = 0;
@@ -117,6 +108,8 @@ export default class MiOS extends EventEmitter {
 		return localStorage.getItem('enableSounds') == 'true';
 	}
 
+	public store: ReturnType<typeof initStore>;
+
 	public apis: API;
 
 	/**
@@ -232,6 +225,11 @@ export default class MiOS extends EventEmitter {
 		console.error.apply(null, args);
 	}
 
+	public bakeMe() {
+		// ローカルストレージにキャッシュ
+		localStorage.setItem('me', JSON.stringify(this.i));
+	}
+
 	public signout() {
 		localStorage.removeItem('me');
 		document.cookie = `i=; domain=${hostname}; expires=Thu, 01 Jan 1970 00:00:01 GMT;`;
@@ -243,6 +241,8 @@ export default class MiOS extends EventEmitter {
 	 * @param callback A function that call when initialized
 	 */
 	public async init(callback) {
+		this.store = initStore(this);
+
 		//#region Init stream managers
 		this.streams.serverStream = new ServerStreamManager(this);
 
@@ -307,16 +307,11 @@ export default class MiOS extends EventEmitter {
 
 		// フェッチが完了したとき
 		const fetched = me => {
-			if (me) {
-				// デフォルトの設定をマージ
-				me.clientSettings = Object.assign(defaultSettings, me.clientSettings);
-
-				// ローカルストレージにキャッシュ
-				localStorage.setItem('me', JSON.stringify(me));
-			}
-
 			this.i = me;
 
+			// ローカルストレージにキャッシュ
+			this.bakeMe();
+
 			this.emit('signedin');
 
 			// Finish init
@@ -333,6 +328,14 @@ export default class MiOS extends EventEmitter {
 		// Get cached account data
 		const cachedMe = JSON.parse(localStorage.getItem('me'));
 
+		//#region キャッシュされた設定を復元
+		const cachedSettings = JSON.parse(localStorage.getItem('settings'));
+
+		if (cachedSettings) {
+			this.store.commit('settings/init', cachedSettings);
+		}
+		//#endregion
+
 		// キャッシュがあったとき
 		if (cachedMe) {
 			if (cachedMe.token == null) {
@@ -346,12 +349,25 @@ export default class MiOS extends EventEmitter {
 			// 後から新鮮なデータをフェッチ
 			fetchme(cachedMe.token, freshData => {
 				merge(cachedMe, freshData);
+
+				this.store.commit('settings/init', freshData.clientSettings);
 			});
 		} else {
 			// Get token from cookie
 			const i = (document.cookie.match(/i=(!\w+)/) || [null, null])[1];
 
-			fetchme(i, fetched);
+			fetchme(i, me => {
+				if (me) {
+					Object.entries(me.clientSettings).forEach(([key, value]) => {
+						this.store.commit('settings/set', { key, value });
+					});
+
+					fetched(me);
+				} else {
+					// Finish init
+					callback();
+				}
+			});
 		}
 	}
 
@@ -456,7 +472,7 @@ export default class MiOS extends EventEmitter {
 		};
 
 		const promise = new Promise((resolve, reject) => {
-			const viaStream = this.stream.hasConnection &&
+			const viaStream = this.stream && this.stream.hasConnection &&
 				(localStorage.getItem('apiViaStream') ? localStorage.getItem('apiViaStream') == 'true' : true);
 
 			if (viaStream) {
diff --git a/src/client/app/common/scripts/streaming/home.ts b/src/client/app/common/scripts/streaming/home.ts
index 73f2c5302..ddb0d4820 100644
--- a/src/client/app/common/scripts/streaming/home.ts
+++ b/src/client/app/common/scripts/streaming/home.ts
@@ -25,10 +25,31 @@ export class HomeStream extends Stream {
 				console.log('I updated:', i);
 			}
 			merge(me, i);
+
+			// キャッシュ更新
+			os.bakeMe();
+		});
+
+		this.on('clientSettingUpdated', x => {
+			os.store.commit('settings/set', {
+				key: x.key,
+				value: x.value
+			});
+		});
+
+		this.on('home_updated', x => {
+			if (x.home) {
+				os.store.commit('settings/setHome', x.home);
+			} else {
+				os.store.commit('settings/setHomeWidget', {
+					id: x.id,
+					data: x.data
+				});
+			}
 		});
 
 		// トークンが再生成されたとき
-		// このままではAPIが利用できないので強制的にサインアウトさせる
+		// このままではMisskeyが利用できないので強制的にサインアウトさせる
 		this.on('my_token_regenerated', () => {
 			alert('%i18n:!common.my-token-regenerated%');
 			os.signout();
diff --git a/src/client/app/common/views/components/avatar.vue b/src/client/app/common/views/components/avatar.vue
new file mode 100644
index 000000000..5aac9c8ba
--- /dev/null
+++ b/src/client/app/common/views/components/avatar.vue
@@ -0,0 +1,38 @@
+<template>
+	<router-link class="mk-avatar" :to="user | userPage" :title="user | acct" :target="target" :style="{ borderRadius: clientSettings.circleIcons ? '100%' : null }">
+		<img v-if="disablePreview" :src="`${user.avatarUrl}?thumbnail&size=128`" alt=""/>
+		<img v-else :src="`${user.avatarUrl}?thumbnail&size=128`" alt="" v-user-preview="user.id"/>
+	</router-link>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+export default Vue.extend({
+	props: {
+		user: {
+			required: true
+		},
+		target: {
+			required: false,
+			default: null
+		},
+		disablePreview: {
+			required: false,
+			default: false
+		}
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+.mk-avatar
+	display block
+
+	> img
+		display inline-block
+		width 100%
+		height 100%
+		margin 0
+		border-radius inherit
+		vertical-align bottom
+</style>
diff --git a/src/client/app/common/views/components/index.ts b/src/client/app/common/views/components/index.ts
index 6bfe43a80..69fed00c7 100644
--- a/src/client/app/common/views/components/index.ts
+++ b/src/client/app/common/views/components/index.ts
@@ -3,6 +3,7 @@ import Vue from 'vue';
 import signin from './signin.vue';
 import signup from './signup.vue';
 import forkit from './forkit.vue';
+import avatar from './avatar.vue';
 import nav from './nav.vue';
 import noteHtml from './note-html';
 import poll from './poll.vue';
@@ -28,6 +29,7 @@ import welcomeTimeline from './welcome-timeline.vue';
 Vue.component('mk-signin', signin);
 Vue.component('mk-signup', signup);
 Vue.component('mk-forkit', forkit);
+Vue.component('mk-avatar', avatar);
 Vue.component('mk-nav', nav);
 Vue.component('mk-note-html', noteHtml);
 Vue.component('mk-poll', poll);
diff --git a/src/client/app/common/views/components/messaging-room.message.vue b/src/client/app/common/views/components/messaging-room.message.vue
index 70df899f5..ba0ab3209 100644
--- a/src/client/app/common/views/components/messaging-room.message.vue
+++ b/src/client/app/common/views/components/messaging-room.message.vue
@@ -1,8 +1,6 @@
 <template>
 <div class="message" :data-is-me="isMe">
-	<router-link class="avatar-anchor" :to="message.user | userPage" :title="message.user | acct" target="_blank">
-		<img class="avatar" :src="`${message.user.avatarUrl}?thumbnail&size=80`" alt=""/>
-	</router-link>
+	<mk-avatar class="avatar" :user="message.user" target="_blank"/>
 	<div class="content">
 		<div class="balloon" :data-no-text="message.text == null">
 			<p class="read" v-if="isMe && message.isRead">%i18n:@is-read%</p>
@@ -67,20 +65,14 @@ export default Vue.extend({
 	padding 10px 12px 10px 12px
 	background-color transparent
 
-	> .avatar-anchor
+	> .avatar
 		display block
 		position absolute
 		top 10px
-
-		> .avatar
-			display block
-			min-width 54px
-			min-height 54px
-			max-width 54px
-			max-height 54px
-			margin 0
-			border-radius 8px
-			transition all 0.1s ease
+		width 54px
+		height 54px
+		border-radius 8px
+		transition all 0.1s ease
 
 	> .content
 
@@ -201,7 +193,7 @@ export default Vue.extend({
 				margin-left 4px
 
 	&:not([data-is-me])
-		> .avatar-anchor
+		> .avatar
 			left 12px
 
 		> .content
@@ -225,7 +217,7 @@ export default Vue.extend({
 				text-align left
 
 	&[data-is-me]
-		> .avatar-anchor
+		> .avatar
 			right 12px
 
 		> .content
diff --git a/src/client/app/common/views/components/messaging.vue b/src/client/app/common/views/components/messaging.vue
index 6f8fcb3a7..11f9c366d 100644
--- a/src/client/app/common/views/components/messaging.vue
+++ b/src/client/app/common/views/components/messaging.vue
@@ -13,7 +13,7 @@
 					@click="navigate(user)"
 					tabindex="-1"
 				>
-					<img class="avatar" :src="`${user.avatarUrl}?thumbnail&size=32`" alt=""/>
+					<mk-avatar class="avatar" :user="user"/>
 					<span class="name">{{ user | userName }}</span>
 					<span class="username">@{{ user | acct }}</span>
 				</li>
@@ -31,7 +31,7 @@
 				:key="message.id"
 			>
 				<div>
-					<img class="avatar" :src="`${isMe(message) ? message.recipient.avatarUrl : message.user.avatarUrl}?thumbnail&size=64`" alt=""/>
+					<mk-avatar class="avatar" :user="isMe(message) ? message.recipient : message.user"/>
 					<header>
 						<span class="name">{{ isMe(message) ? message.recipient : message.user | userName }}</span>
 						<span class="username">@{{ isMe(message) ? message.recipient : message.user | acct }}</span>
diff --git a/src/client/app/common/views/components/welcome-timeline.vue b/src/client/app/common/views/components/welcome-timeline.vue
index 349797690..6fadb030c 100644
--- a/src/client/app/common/views/components/welcome-timeline.vue
+++ b/src/client/app/common/views/components/welcome-timeline.vue
@@ -1,9 +1,7 @@
 <template>
 <div class="mk-welcome-timeline">
 	<div v-for="note in notes">
-		<router-link class="avatar-anchor" :to="note.user | userPage" v-user-preview="note.user.id">
-			<img class="avatar" :src="`${note.user.avatarUrl}?thumbnail&size=96`" alt="avatar"/>
-		</router-link>
+		<mk-avatar class="avatar" :user="note.user" target="_blank"/>
 		<div class="body">
 			<header>
 				<router-link class="name" :to="note.user | userPage" v-user-preview="note.user.id">{{ note.user | userName }}</router-link>
@@ -69,18 +67,15 @@ export default Vue.extend({
 			display block
 			clear both
 
-		> .avatar-anchor
+		> .avatar
 			display block
 			float left
 			position -webkit-sticky
 			position sticky
 			top 16px
-
-			> img
-				display block
-				width 42px
-				height 42px
-				border-radius 6px
+			width 42px
+			height 42px
+			border-radius 6px
 
 		> .body
 			float right
diff --git a/src/client/app/common/views/widgets/access-log.vue b/src/client/app/common/views/widgets/access-log.vue
index 0b1c7fe2d..8652e3564 100644
--- a/src/client/app/common/views/widgets/access-log.vue
+++ b/src/client/app/common/views/widgets/access-log.vue
@@ -61,6 +61,7 @@ export default define({
 			} else {
 				this.props.design++;
 			}
+			this.save();
 		}
 	}
 });
diff --git a/src/client/app/common/views/widgets/broadcast.vue b/src/client/app/common/views/widgets/broadcast.vue
index 96d1d0ef3..75b1d6052 100644
--- a/src/client/app/common/views/widgets/broadcast.vue
+++ b/src/client/app/common/views/widgets/broadcast.vue
@@ -68,6 +68,7 @@ export default define({
 			} else {
 				this.props.design++;
 			}
+			this.save();
 		}
 	}
 });
diff --git a/src/client/app/common/views/widgets/calendar.vue b/src/client/app/common/views/widgets/calendar.vue
index 0bb503759..41e925378 100644
--- a/src/client/app/common/views/widgets/calendar.vue
+++ b/src/client/app/common/views/widgets/calendar.vue
@@ -73,6 +73,7 @@ export default define({
 			} else {
 				this.props.design++;
 			}
+			this.save();
 		},
 		tick() {
 			const now = new Date();
diff --git a/src/client/app/common/views/widgets/photo-stream.vue b/src/client/app/common/views/widgets/photo-stream.vue
index c51d932bd..ae5924bb1 100644
--- a/src/client/app/common/views/widgets/photo-stream.vue
+++ b/src/client/app/common/views/widgets/photo-stream.vue
@@ -59,6 +59,8 @@ export default define({
 			} else {
 				this.props.design++;
 			}
+
+			this.save();
 		}
 	}
 });
diff --git a/src/client/app/common/views/widgets/rss.vue b/src/client/app/common/views/widgets/rss.vue
index f0ba11678..b5339add0 100644
--- a/src/client/app/common/views/widgets/rss.vue
+++ b/src/client/app/common/views/widgets/rss.vue
@@ -40,6 +40,7 @@ export default define({
 	methods: {
 		func() {
 			this.props.compact = !this.props.compact;
+			this.save();
 		},
 		fetch() {
 			fetch(`https://api.rss2json.com/v1/api.json?rss_url=${this.url}`, {
diff --git a/src/client/app/common/views/widgets/server.vue b/src/client/app/common/views/widgets/server.vue
index 2fbc07adf..2fdd60499 100644
--- a/src/client/app/common/views/widgets/server.vue
+++ b/src/client/app/common/views/widgets/server.vue
@@ -68,6 +68,7 @@ export default define({
 			} else {
 				this.props.view++;
 			}
+			this.save();
 		},
 		func() {
 			if (this.props.design == 2) {
@@ -75,6 +76,7 @@ export default define({
 			} else {
 				this.props.design++;
 			}
+			this.save();
 		}
 	}
 });
diff --git a/src/client/app/common/views/widgets/slideshow.vue b/src/client/app/common/views/widgets/slideshow.vue
index 95be4b94f..459b24a32 100644
--- a/src/client/app/common/views/widgets/slideshow.vue
+++ b/src/client/app/common/views/widgets/slideshow.vue
@@ -64,6 +64,7 @@ export default define({
 			} else {
 				this.props.size++;
 			}
+			this.save();
 
 			this.applySize();
 		},
@@ -111,6 +112,7 @@ export default define({
 		choose() {
 			(this as any).apis.chooseDriveFolder().then(folder => {
 				this.props.folder = folder ? folder.id : null;
+				this.save();
 				this.fetch();
 			});
 		}
diff --git a/src/client/app/desktop/views/components/friends-maker.vue b/src/client/app/desktop/views/components/friends-maker.vue
index af5bde3ad..3c1f8b825 100644
--- a/src/client/app/desktop/views/components/friends-maker.vue
+++ b/src/client/app/desktop/views/components/friends-maker.vue
@@ -3,9 +3,7 @@
 	<p class="title">気になるユーザーをフォロー:</p>
 	<div class="users" v-if="!fetching && users.length > 0">
 		<div class="user" v-for="user in users" :key="user.id">
-			<router-link class="avatar-anchor" :to="user | userPage">
-				<img class="avatar" :src="`${user.avatarUrl}?thumbnail&size=42`" alt="" v-user-preview="user.id"/>
-			</router-link>
+			<mk-avatar class="avatar" :user="user" target="_blank"/>
 			<div class="body">
 				<router-link class="name" :to="user | userPage" v-user-preview="user.id">{{ user | userName }}</router-link>
 				<p class="username">@{{ user | acct }}</p>
@@ -86,18 +84,13 @@ export default Vue.extend({
 				display block
 				clear both
 
-			> .avatar-anchor
+			> .avatar
 				display block
 				float left
 				margin 0 12px 0 0
-
-				> .avatar
-					display block
-					width 42px
-					height 42px
-					margin 0
-					border-radius 8px
-					vertical-align bottom
+				width 42px
+				height 42px
+				border-radius 8px
 
 			> .body
 				float left
diff --git a/src/client/app/desktop/views/components/home.vue b/src/client/app/desktop/views/components/home.vue
index 4343a7fb7..5337be61f 100644
--- a/src/client/app/desktop/views/components/home.vue
+++ b/src/client/app/desktop/views/components/home.vue
@@ -53,7 +53,7 @@
 			<div class="main">
 				<a @click="hint">カスタマイズのヒント</a>
 				<div>
-					<mk-post-form v-if="os.i.clientSettings.showPostFormOnTopOfTl"/>
+					<mk-post-form v-if="clientSettings.showPostFormOnTopOfTl"/>
 					<mk-timeline ref="tl" @loaded="onTlLoaded"/>
 				</div>
 			</div>
@@ -63,7 +63,7 @@
 				<component v-for="widget in widgets[place]" :is="`mkw-${widget.name}`" :key="widget.id" :ref="widget.id" :widget="widget" @chosen="warp"/>
 			</div>
 			<div class="main">
-				<mk-post-form v-if="os.i.clientSettings.showPostFormOnTopOfTl"/>
+				<mk-post-form v-if="clientSettings.showPostFormOnTopOfTl"/>
 				<mk-timeline ref="tl" @loaded="onTlLoaded" v-if="mode == 'timeline'"/>
 				<mk-mentions @loaded="onTlLoaded" v-if="mode == 'mentions'"/>
 			</div>
@@ -81,6 +81,7 @@ export default Vue.extend({
 	components: {
 		XDraggable
 	},
+
 	props: {
 		customize: {
 			type: Boolean,
@@ -91,61 +92,43 @@ export default Vue.extend({
 			default: 'timeline'
 		}
 	},
+
 	data() {
 		return {
 			connection: null,
 			connectionId: null,
 			widgetAdderSelected: null,
-			trash: [],
-			widgets: {
-				left: [],
-				right: []
-			}
+			trash: []
 		};
 	},
+
 	computed: {
-		home: {
-			get(): any[] {
-				//#region 互換性のため
-				(this as any).os.i.clientSettings.home.forEach(w => {
-					if (w.name == 'rss-reader') w.name = 'rss';
-					if (w.name == 'user-recommendation') w.name = 'users';
-					if (w.name == 'recommended-polls') w.name = 'polls';
-				});
-				//#endregion
-				return (this as any).os.i.clientSettings.home;
-			},
-			set(value) {
-				(this as any).os.i.clientSettings.home = value;
-			}
+		home(): any[] {
+			return this.$store.state.settings.data.home;
 		},
 		left(): any[] {
 			return this.home.filter(w => w.place == 'left');
 		},
 		right(): any[] {
 			return this.home.filter(w => w.place == 'right');
+		},
+		widgets(): any {
+			return {
+				left: this.left,
+				right: this.right
+			};
 		}
 	},
-	created() {
-		this.widgets.left = this.left;
-		this.widgets.right = this.right;
-		this.$watch('os.i.clientSettings', i => {
-			this.widgets.left = this.left;
-			this.widgets.right = this.right;
-		}, {
-			deep: true
-		});
-	},
+
 	mounted() {
 		this.connection = (this as any).os.stream.getConnection();
 		this.connectionId = (this as any).os.stream.use();
-
-		this.connection.on('home_updated', this.onHomeUpdated);
 	},
+
 	beforeDestroy() {
-		this.connection.off('home_updated', this.onHomeUpdated);
 		(this as any).os.stream.dispose(this.connectionId);
 	},
+
 	methods: {
 		hint() {
 			(this as any).apis.dialog({
@@ -159,56 +142,44 @@ export default Vue.extend({
 				}]
 			});
 		},
+
 		onTlLoaded() {
 			this.$emit('loaded');
 		},
-		onHomeUpdated(data) {
-			if (data.home) {
-				(this as any).os.i.clientSettings.home = data.home;
-				this.widgets.left = data.home.filter(w => w.place == 'left');
-				this.widgets.right = data.home.filter(w => w.place == 'right');
-			} else {
-				const w = (this as any).os.i.clientSettings.home.find(w => w.id == data.id);
-				if (w != null) {
-					w.data = data.data;
-					this.$refs[w.id][0].preventSave = true;
-					this.$refs[w.id][0].props = w.data;
-					this.widgets.left = (this as any).os.i.clientSettings.home.filter(w => w.place == 'left');
-					this.widgets.right = (this as any).os.i.clientSettings.home.filter(w => w.place == 'right');
-				}
-			}
-		},
+
 		onWidgetContextmenu(widgetId) {
 			const w = (this.$refs[widgetId] as any)[0];
 			if (w.func) w.func();
 		},
+
 		onWidgetSort() {
 			this.saveHome();
 		},
+
 		onTrash(evt) {
 			this.saveHome();
 		},
+
 		addWidget() {
-			const widget = {
+			this.$store.dispatch('settings/addHomeWidget', {
 				name: this.widgetAdderSelected,
 				id: uuid(),
 				place: 'left',
 				data: {}
-			};
-
-			this.widgets.left.unshift(widget);
-			this.saveHome();
+			});
 		},
+
 		saveHome() {
 			const left = this.widgets.left;
 			const right = this.widgets.right;
-			this.home = left.concat(right);
+			this.$store.commit('settings/setHome', left.concat(right));
 			left.forEach(w => w.place = 'left');
 			right.forEach(w => w.place = 'right');
 			(this as any).api('i/update_home', {
 				home: this.home
 			});
 		},
+
 		warp(date) {
 			(this.$refs.tl as any).warp(date);
 		}
diff --git a/src/client/app/desktop/views/components/note-detail.sub.vue b/src/client/app/desktop/views/components/note-detail.sub.vue
index 5175c8bd4..24550c4e9 100644
--- a/src/client/app/desktop/views/components/note-detail.sub.vue
+++ b/src/client/app/desktop/views/components/note-detail.sub.vue
@@ -1,8 +1,6 @@
 <template>
 <div class="sub" :title="title">
-	<router-link class="avatar-anchor" :to="note.user | userPage">
-		<img class="avatar" :src="`${note.user.avatarUrl}?thumbnail&size=64`" alt="avatar" v-user-preview="note.userId"/>
-	</router-link>
+	<mk-avatar class="avatar" :user="note.user"/>
 	<div class="main">
 		<header>
 			<div class="left">
@@ -57,18 +55,13 @@ root(isDark)
 		> .main > footer > button
 			color #888
 
-	> .avatar-anchor
+	> .avatar
 		display block
 		float left
 		margin 0 16px 0 0
-
-		> .avatar
-			display block
-			width 44px
-			height 44px
-			margin 0
-			border-radius 4px
-			vertical-align bottom
+		width 44px
+		height 44px
+		border-radius 4px
 
 	> .main
 		float left
diff --git a/src/client/app/desktop/views/components/note-detail.vue b/src/client/app/desktop/views/components/note-detail.vue
index 525023349..5d07dc90d 100644
--- a/src/client/app/desktop/views/components/note-detail.vue
+++ b/src/client/app/desktop/views/components/note-detail.vue
@@ -18,18 +18,14 @@
 	</div>
 	<div class="renote" v-if="isRenote">
 		<p>
-			<router-link class="avatar-anchor" :to="note.user | userPage" v-user-preview="note.userId">
-				<img class="avatar" :src="`${note.user.avatarUrl}?thumbnail&size=32`" alt="avatar"/>
-			</router-link>
+			<mk-avatar class="avatar" :user="note.user"/>
 			%fa:retweet%
 			<router-link class="name" :href="note.user | userPage">{{ note.user | userName }}</router-link>
 			がRenote
 		</p>
 	</div>
 	<article>
-		<router-link class="avatar-anchor" :to="p.user | userPage">
-			<img class="avatar" :src="`${p.user.avatarUrl}?thumbnail&size=64`" alt="avatar" v-user-preview="p.user.id"/>
-		</router-link>
+		<mk-avatar class="avatar" :user="p.user"/>
 		<header>
 			<router-link class="name" :to="p.user | userPage" v-user-preview="p.user.id">{{ p.user | userName }}</router-link>
 			<span class="username">@{{ p.user | acct }}</span>
@@ -159,7 +155,7 @@ export default Vue.extend({
 
 		// Draw map
 		if (this.p.geo) {
-			const shouldShowMap = (this as any).os.isSignedIn ? (this as any).os.i.clientSettings.showMaps : true;
+			const shouldShowMap = (this as any).os.isSignedIn ? (this as any).clientSettings.showMaps : true;
 			if (shouldShowMap) {
 				(this as any).os.getGoogleMaps().then(maps => {
 					const uluru = new maps.LatLng(this.p.geo.coordinates[1], this.p.geo.coordinates[0]);
@@ -262,17 +258,12 @@ root(isDark)
 			margin 0
 			padding 16px 32px
 
-			.avatar-anchor
+			.avatar
 				display inline-block
-
-				.avatar
-					vertical-align bottom
-					min-width 28px
-					min-height 28px
-					max-width 28px
-					max-height 28px
-					margin 0 8px 0 0
-					border-radius 6px
+				width 28px
+				height 28px
+				margin 0 8px 0 0
+				border-radius 6px
 
 			[data-fa]
 				margin-right 4px
@@ -298,18 +289,10 @@ root(isDark)
 			> footer > button
 				color isDark ? #707b97 : #888
 
-		> .avatar-anchor
-			display block
+		> .avatar
 			width 60px
 			height 60px
-
-			> .avatar
-				display block
-				width 60px
-				height 60px
-				margin 0
-				border-radius 8px
-				vertical-align bottom
+			border-radius 8px
 
 		> header
 			position absolute
diff --git a/src/client/app/desktop/views/components/note-preview.vue b/src/client/app/desktop/views/components/note-preview.vue
index b45814e51..43eb15988 100644
--- a/src/client/app/desktop/views/components/note-preview.vue
+++ b/src/client/app/desktop/views/components/note-preview.vue
@@ -1,8 +1,6 @@
 <template>
 <div class="mk-note-preview" :title="title">
-	<router-link class="avatar-anchor" :to="note.user | userPage">
-		<img class="avatar" :src="`${note.user.avatarUrl}?thumbnail&size=64`" alt="avatar" v-user-preview="note.userId"/>
-	</router-link>
+	<mk-avatar class="avatar" :user="note.user"/>
 	<div class="main">
 		<header>
 			<router-link class="name" :to="note.user | userPage" v-user-preview="note.userId">{{ note.user | userName }}</router-link>
@@ -41,18 +39,13 @@ root(isDark)
 		display block
 		clear both
 
-	> .avatar-anchor
+	> .avatar
 		display block
 		float left
 		margin 0 16px 0 0
-
-		> .avatar
-			display block
-			width 52px
-			height 52px
-			margin 0
-			border-radius 8px
-			vertical-align bottom
+		width 52px
+		height 52px
+		border-radius 8px
 
 	> .main
 		float left
diff --git a/src/client/app/desktop/views/components/notes.note.sub.vue b/src/client/app/desktop/views/components/notes.note.sub.vue
index 4472ddefb..238fb0369 100644
--- a/src/client/app/desktop/views/components/notes.note.sub.vue
+++ b/src/client/app/desktop/views/components/notes.note.sub.vue
@@ -1,8 +1,6 @@
 <template>
 <div class="sub" :title="title">
-	<router-link class="avatar-anchor" :to="note.user | userPage">
-		<img class="avatar" :src="`${note.user.avatarUrl}?thumbnail&size=64`" alt="avatar" v-user-preview="note.userId"/>
-	</router-link>
+	<mk-avatar class="avatar" :user="note.user"/>
 	<div class="main">
 		<header>
 			<router-link class="name" :to="note.user | userPage" v-user-preview="note.userId">{{ note.user | userName }}</router-link>
@@ -53,18 +51,13 @@ root(isDark)
 		display block
 		clear both
 
-	> .avatar-anchor
+	> .avatar
 		display block
 		float left
 		margin 0 14px 0 0
-
-		> .avatar
-			display block
-			width 52px
-			height 52px
-			margin 0
-			border-radius 8px
-			vertical-align bottom
+		width 52px
+		height 52px
+		border-radius 8px
 
 	> .main
 		float left
diff --git a/src/client/app/desktop/views/components/notes.note.vue b/src/client/app/desktop/views/components/notes.note.vue
index ee24543eb..b512f78ec 100644
--- a/src/client/app/desktop/views/components/notes.note.vue
+++ b/src/client/app/desktop/views/components/notes.note.vue
@@ -1,12 +1,10 @@
 <template>
 <div class="note" tabindex="-1" :title="title" @keydown="onKeydown">
-	<div class="reply-to" v-if="p.reply && (!os.isSignedIn || os.i.clientSettings.showReplyTarget)">
+	<div class="reply-to" v-if="p.reply && (!os.isSignedIn || clientSettings.showReplyTarget)">
 		<x-sub :note="p.reply"/>
 	</div>
 	<div class="renote" v-if="isRenote">
-		<router-link class="avatar-anchor" :to="note.user | userPage" v-user-preview="note.userId">
-			<img class="avatar" :src="`${note.user.avatarUrl}?thumbnail&size=32`" alt="avatar"/>
-		</router-link>
+		<mk-avatar class="avatar" :user="note.user"/>
 		%fa:retweet%
 		<span>{{ '%i18n:!@reposted-by%'.substr(0, '%i18n:!@reposted-by%'.indexOf('{')) }}</span>
 		<a class="name" :href="note.user | userPage" v-user-preview="note.userId">{{ note.user | userName }}</a>
@@ -14,9 +12,7 @@
 		<mk-time :time="note.createdAt"/>
 	</div>
 	<article>
-		<router-link class="avatar-anchor" :to="p.user | userPage">
-			<img class="avatar" :src="`${p.user.avatarUrl}?thumbnail&size=64`" alt="avatar" v-user-preview="p.user.id"/>
-		</router-link>
+		<mk-avatar class="avatar" :user="p.user"/>
 		<div class="main">
 			<header>
 				<router-link class="name" :to="p.user | userPage" v-user-preview="p.user.id">{{ p.user | userName }}</router-link>
@@ -182,7 +178,7 @@ export default Vue.extend({
 
 		// Draw map
 		if (this.p.geo) {
-			const shouldShowMap = (this as any).os.isSignedIn ? (this as any).os.i.clientSettings.showMaps : true;
+			const shouldShowMap = (this as any).os.isSignedIn ? (this as any).clientSettings.showMaps : true;
 			if (shouldShowMap) {
 				(this as any).os.getGoogleMaps().then(maps => {
 					const uluru = new maps.LatLng(this.p.geo.coordinates[1], this.p.geo.coordinates[0]);
@@ -343,15 +339,12 @@ root(isDark)
 		color #9dbb00
 		background isDark ? linear-gradient(to bottom, #314027 0%, #282c37 100%) : linear-gradient(to bottom, #edfde2 0%, #fff 100%)
 
-		.avatar-anchor
+		.avatar
 			display inline-block
-
-			.avatar
-				vertical-align bottom
-				width 28px
-				height 28px
-				margin 0 8px 0 0
-				border-radius 6px
+			width 28px
+			height 28px
+			margin 0 8px 0 0
+			border-radius 6px
 
 		[data-fa]
 			margin-right 4px
@@ -390,22 +383,17 @@ root(isDark)
 			> .main > footer > button
 				color isDark ? #707b97 : #888
 
-		> .avatar-anchor
+		> .avatar
 			display block
 			float left
 			margin 0 16px 10px 0
+			width 58px
+			height 58px
+			border-radius 8px
 			//position -webkit-sticky
 			//position sticky
 			//top 74px
 
-			> .avatar
-				display block
-				width 58px
-				height 58px
-				margin 0
-				border-radius 8px
-				vertical-align bottom
-
 		> .main
 			float left
 			width calc(100% - 74px)
diff --git a/src/client/app/desktop/views/components/notes.vue b/src/client/app/desktop/views/components/notes.vue
index c4824feea..7e80e6f74 100644
--- a/src/client/app/desktop/views/components/notes.vue
+++ b/src/client/app/desktop/views/components/notes.vue
@@ -121,13 +121,13 @@ export default Vue.extend({
 			const isMyNote = note.userId == (this as any).os.i.id;
 			const isPureRenote = note.renoteId != null && note.text == null && note.mediaIds.length == 0 && note.poll == null;
 
-			if ((this as any).os.i.clientSettings.showMyRenotes === false) {
+			if ((this as any).clientSettings.showMyRenotes === false) {
 				if (isMyNote && isPureRenote) {
 					return;
 				}
 			}
 
-			if ((this as any).os.i.clientSettings.showRenotedMyNotes === false) {
+			if ((this as any).clientSettings.showRenotedMyNotes === false) {
 				if (isPureRenote && (note.renote.userId == (this as any).os.i.id)) {
 					return;
 				}
@@ -199,7 +199,7 @@ export default Vue.extend({
 				this.clearNotification();
 			}
 
-			if ((this as any).os.i.clientSettings.fetchOnScroll !== false) {
+			if ((this as any).clientSettings.fetchOnScroll !== false) {
 				const current = window.scrollY + window.innerHeight;
 				if (current > document.body.offsetHeight - 8) this.loadMore();
 			}
diff --git a/src/client/app/desktop/views/components/notifications.vue b/src/client/app/desktop/views/components/notifications.vue
index 36e9dce6a..7923d1a62 100644
--- a/src/client/app/desktop/views/components/notifications.vue
+++ b/src/client/app/desktop/views/components/notifications.vue
@@ -6,9 +6,7 @@
 				<div class="notification" :class="notification.type" :key="notification.id">
 					<mk-time :time="notification.createdAt"/>
 					<template v-if="notification.type == 'reaction'">
-						<router-link class="avatar-anchor" :to="notification.user | userPage" v-user-preview="notification.user.id">
-							<img class="avatar" :src="`${notification.user.avatarUrl}?thumbnail&size=48`" alt="avatar"/>
-						</router-link>
+						<mk-avatar class="avatar" :user="notification.user"/>
 						<div class="text">
 							<p>
 								<mk-reaction-icon :reaction="notification.reaction"/>
@@ -20,9 +18,7 @@
 						</div>
 					</template>
 					<template v-if="notification.type == 'renote'">
-						<router-link class="avatar-anchor" :to="notification.note.user | userPage" v-user-preview="notification.note.userId">
-							<img class="avatar" :src="`${notification.note.user.avatarUrl}?thumbnail&size=48`" alt="avatar"/>
-						</router-link>
+						<mk-avatar class="avatar" :user="notification.note.user"/>
 						<div class="text">
 							<p>%fa:retweet%
 								<router-link :to="notification.note.user | userPage" v-user-preview="notification.note.userId">{{ notification.note.user | userName }}</router-link>
@@ -33,9 +29,7 @@
 						</div>
 					</template>
 					<template v-if="notification.type == 'quote'">
-						<router-link class="avatar-anchor" :to="notification.note.user | userPage" v-user-preview="notification.note.userId">
-							<img class="avatar" :src="`${notification.note.user.avatarUrl}?thumbnail&size=48`" alt="avatar"/>
-						</router-link>
+						<mk-avatar class="avatar" :user="notification.note.user"/>
 						<div class="text">
 							<p>%fa:quote-left%
 								<router-link :to="notification.note.user | userPage" v-user-preview="notification.note.userId">{{ notification.note.user | userName }}</router-link>
@@ -44,9 +38,7 @@
 						</div>
 					</template>
 					<template v-if="notification.type == 'follow'">
-						<router-link class="avatar-anchor" :to="notification.user | userPage" v-user-preview="notification.user.id">
-							<img class="avatar" :src="`${notification.user.avatarUrl}?thumbnail&size=48`" alt="avatar"/>
-						</router-link>
+						<mk-avatar class="avatar" :user="notification.user"/>
 						<div class="text">
 							<p>%fa:user-plus%
 								<router-link :to="notification.user | userPage" v-user-preview="notification.user.id">{{ notification.user | userName }}</router-link>
@@ -54,9 +46,7 @@
 						</div>
 					</template>
 					<template v-if="notification.type == 'reply'">
-						<router-link class="avatar-anchor" :to="notification.note.user | userPage" v-user-preview="notification.note.userId">
-							<img class="avatar" :src="`${notification.note.user.avatarUrl}?thumbnail&size=48`" alt="avatar"/>
-						</router-link>
+						<mk-avatar class="avatar" :user="notification.note.user"/>
 						<div class="text">
 							<p>%fa:reply%
 								<router-link :to="notification.note.user | userPage" v-user-preview="notification.note.userId">{{ notification.note.user | userName }}</router-link>
@@ -65,9 +55,7 @@
 						</div>
 					</template>
 					<template v-if="notification.type == 'mention'">
-						<router-link class="avatar-anchor" :to="notification.note.user | userPage" v-user-preview="notification.note.userId">
-							<img class="avatar" :src="`${notification.note.user.avatarUrl}?thumbnail&size=48`" alt="avatar"/>
-						</router-link>
+						<mk-avatar class="avatar" :user="notification.note.user"/>
 						<div class="text">
 							<p>%fa:at%
 								<router-link :to="notification.note.user | userPage" v-user-preview="notification.note.userId">{{ notification.note.user | userName }}</router-link>
@@ -76,9 +64,7 @@
 						</div>
 					</template>
 					<template v-if="notification.type == 'poll_vote'">
-						<router-link class="avatar-anchor" :to="notification.user | userPage" v-user-preview="notification.user.id">
-							<img class="avatar" :src="`${notification.user.avatarUrl}?thumbnail&size=48`" alt="avatar"/>
-						</router-link>
+						<mk-avatar class="avatar" :user="notification.user"/>
 						<div class="text">
 							<p>%fa:chart-pie%<a :href="notification.user | userPage" v-user-preview="notification.user.id">{{ notification.user | userName }}</a></p>
 							<router-link class="note-ref" :to="notification.note | notePage">
@@ -223,20 +209,15 @@ root(isDark)
 					display block
 					clear both
 
-				> .avatar-anchor
+				> .avatar
 					display block
 					float left
 					position -webkit-sticky
 					position sticky
 					top 16px
-
-					> img
-						display block
-						min-width 36px
-						min-height 36px
-						max-width 36px
-						max-height 36px
-						border-radius 6px
+					width 36px
+					height 36px
+					border-radius 6px
 
 				> .text
 					float right
diff --git a/src/client/app/desktop/views/components/settings.vue b/src/client/app/desktop/views/components/settings.vue
index 9d56042ea..af6ab1266 100644
--- a/src/client/app/desktop/views/components/settings.vue
+++ b/src/client/app/desktop/views/components/settings.vue
@@ -20,7 +20,7 @@
 
 		<section class="web" v-show="page == 'web'">
 			<h1>動作</h1>
-			<mk-switch v-model="os.i.clientSettings.fetchOnScroll" @change="onChangeFetchOnScroll" text="スクロールで自動読み込み">
+			<mk-switch v-model="clientSettings.fetchOnScroll" @change="onChangeFetchOnScroll" text="スクロールで自動読み込み">
 				<span>ページを下までスクロールしたときに自動で追加のコンテンツを読み込みます。</span>
 			</mk-switch>
 			<mk-switch v-model="autoPopout" text="ウィンドウの自動ポップアウト">
@@ -41,13 +41,14 @@
 			</div>
 			<div class="div">
 				<mk-switch v-model="darkmode" text="ダークモード"/>
-				<mk-switch v-model="os.i.clientSettings.gradientWindowHeader" @change="onChangeGradientWindowHeader" text="ウィンドウのタイトルバーにグラデーションを使用"/>
+				<mk-switch v-model="clientSettings.circleIcons" @change="onChangeCircleIcons" text="丸いアイコンを使用"/>
+				<mk-switch v-model="clientSettings.gradientWindowHeader" @change="onChangeGradientWindowHeader" text="ウィンドウのタイトルバーにグラデーションを使用"/>
 			</div>
-			<mk-switch v-model="os.i.clientSettings.showPostFormOnTopOfTl" @change="onChangeShowPostFormOnTopOfTl" text="タイムライン上部に投稿フォームを表示する"/>
-			<mk-switch v-model="os.i.clientSettings.showReplyTarget" @change="onChangeShowReplyTarget" text="リプライ先を表示する"/>
-			<mk-switch v-model="os.i.clientSettings.showMyRenotes" @change="onChangeShowMyRenotes" text="自分の行ったRenoteをタイムラインに表示する"/>
-			<mk-switch v-model="os.i.clientSettings.showRenotedMyNotes" @change="onChangeShowRenotedMyNotes" text="Renoteされた自分の投稿をタイムラインに表示する"/>
-			<mk-switch v-model="os.i.clientSettings.showMaps" @change="onChangeShowMaps" text="マップの自動展開">
+			<mk-switch v-model="clientSettings.showPostFormOnTopOfTl" @change="onChangeShowPostFormOnTopOfTl" text="タイムライン上部に投稿フォームを表示する"/>
+			<mk-switch v-model="clientSettings.showReplyTarget" @change="onChangeShowReplyTarget" text="リプライ先を表示する"/>
+			<mk-switch v-model="clientSettings.showMyRenotes" @change="onChangeShowMyRenotes" text="自分の行ったRenoteをタイムラインに表示する"/>
+			<mk-switch v-model="clientSettings.showRenotedMyNotes" @change="onChangeShowRenotedMyNotes" text="Renoteされた自分の投稿をタイムラインに表示する"/>
+			<mk-switch v-model="clientSettings.showMaps" @change="onChangeShowMaps" text="マップの自動展開">
 				<span>位置情報が添付された投稿のマップを自動的に展開します。</span>
 			</mk-switch>
 		</section>
@@ -69,7 +70,7 @@
 
 		<section class="web" v-show="page == 'web'">
 			<h1>モバイル</h1>
-			<mk-switch v-model="os.i.clientSettings.disableViaMobile" @change="onChangeDisableViaMobile" text="「モバイルからの投稿」フラグを付けない"/>
+			<mk-switch v-model="clientSettings.disableViaMobile" @change="onChangeDisableViaMobile" text="「モバイルからの投稿」フラグを付けない"/>
 		</section>
 
 		<section class="web" v-show="page == 'web'">
@@ -297,8 +298,8 @@ export default Vue.extend({
 			this.$emit('done');
 		},
 		onChangeFetchOnScroll(v) {
-			(this as any).api('i/update_client_setting', {
-				name: 'fetchOnScroll',
+			this.$store.dispatch('settings/set', {
+				key: 'fetchOnScroll',
 				value: v
 			});
 		},
@@ -308,50 +309,56 @@ export default Vue.extend({
 			});
 		},
 		onChangeDark(v) {
-			(this as any).api('i/update_client_setting', {
-				name: 'dark',
+			this.$store.dispatch('settings/set', {
+				key: 'dark',
 				value: v
 			});
 		},
 		onChangeShowPostFormOnTopOfTl(v) {
-			(this as any).api('i/update_client_setting', {
-				name: 'showPostFormOnTopOfTl',
+			this.$store.dispatch('settings/set', {
+				key: 'showPostFormOnTopOfTl',
 				value: v
 			});
 		},
 		onChangeShowReplyTarget(v) {
-			(this as any).api('i/update_client_setting', {
-				name: 'showReplyTarget',
+			this.$store.dispatch('settings/set', {
+				key: 'showReplyTarget',
 				value: v
 			});
 		},
 		onChangeShowMyRenotes(v) {
-			(this as any).api('i/update_client_setting', {
-				name: 'showMyRenotes',
+			this.$store.dispatch('settings/set', {
+				key: 'showMyRenotes',
 				value: v
 			});
 		},
 		onChangeShowRenotedMyNotes(v) {
-			(this as any).api('i/update_client_setting', {
-				name: 'showRenotedMyNotes',
+			this.$store.dispatch('settings/set', {
+				key: 'showRenotedMyNotes',
 				value: v
 			});
 		},
 		onChangeShowMaps(v) {
-			(this as any).api('i/update_client_setting', {
-				name: 'showMaps',
+			this.$store.dispatch('settings/set', {
+				key: 'showMaps',
+				value: v
+			});
+		},
+		onChangeCircleIcons(v) {
+			this.$store.dispatch('settings/set', {
+				key: 'circleIcons',
 				value: v
 			});
 		},
 		onChangeGradientWindowHeader(v) {
-			(this as any).api('i/update_client_setting', {
-				name: 'gradientWindowHeader',
+			this.$store.dispatch('settings/set', {
+				key: 'gradientWindowHeader',
 				value: v
 			});
 		},
 		onChangeDisableViaMobile(v) {
-			(this as any).api('i/update_client_setting', {
-				name: 'disableViaMobile',
+			this.$store.dispatch('settings/set', {
+				key: 'disableViaMobile',
 				value: v
 			});
 		},
diff --git a/src/client/app/desktop/views/components/timeline.core.vue b/src/client/app/desktop/views/components/timeline.core.vue
index a137a5707..254a5b9d6 100644
--- a/src/client/app/desktop/views/components/timeline.core.vue
+++ b/src/client/app/desktop/views/components/timeline.core.vue
@@ -101,8 +101,8 @@ export default Vue.extend({
 				(this as any).api(this.endpoint, {
 					limit: fetchLimit + 1,
 					untilDate: this.date ? this.date.getTime() : undefined,
-					includeMyRenotes: (this as any).os.i.clientSettings.showMyRenotes,
-					includeRenotedMyNotes: (this as any).os.i.clientSettings.showRenotedMyNotes
+					includeMyRenotes: (this as any).clientSettings.showMyRenotes,
+					includeRenotedMyNotes: (this as any).clientSettings.showRenotedMyNotes
 				}).then(notes => {
 					if (notes.length == fetchLimit + 1) {
 						notes.pop();
@@ -123,8 +123,8 @@ export default Vue.extend({
 			(this as any).api(this.endpoint, {
 				limit: fetchLimit + 1,
 				untilId: (this.$refs.timeline as any).tail().id,
-				includeMyRenotes: (this as any).os.i.clientSettings.showMyRenotes,
-				includeRenotedMyNotes: (this as any).os.i.clientSettings.showRenotedMyNotes
+				includeMyRenotes: (this as any).clientSettings.showMyRenotes,
+				includeRenotedMyNotes: (this as any).clientSettings.showRenotedMyNotes
 			}).then(notes => {
 				if (notes.length == fetchLimit + 1) {
 					notes.pop();
diff --git a/src/client/app/desktop/views/components/ui.header.account.vue b/src/client/app/desktop/views/components/ui.header.account.vue
index 145666b4b..897d3de81 100644
--- a/src/client/app/desktop/views/components/ui.header.account.vue
+++ b/src/client/app/desktop/views/components/ui.header.account.vue
@@ -2,7 +2,7 @@
 <div class="account">
 	<button class="header" :data-active="isOpen" @click="toggle">
 		<span class="username">{{ os.i.username }}<template v-if="!isOpen">%fa:angle-down%</template><template v-if="isOpen">%fa:angle-up%</template></span>
-		<img class="avatar" :src="`${ os.i.avatarUrl }?thumbnail&size=64`" alt="avatar"/>
+		<mk-avatar class="avatar" :user="os.i"/>
 	</button>
 	<transition name="zoom-in-top">
 		<div class="menu" v-if="isOpen">
diff --git a/src/client/app/desktop/views/components/user-list-timeline.vue b/src/client/app/desktop/views/components/user-list-timeline.vue
index ee983a969..ccdf8f64d 100644
--- a/src/client/app/desktop/views/components/user-list-timeline.vue
+++ b/src/client/app/desktop/views/components/user-list-timeline.vue
@@ -46,8 +46,8 @@ export default Vue.extend({
 				(this as any).api('notes/user-list-timeline', {
 					listId: this.list.id,
 					limit: fetchLimit + 1,
-					includeMyRenotes: (this as any).os.i.clientSettings.showMyRenotes,
-					includeRenotedMyNotes: (this as any).os.i.clientSettings.showRenotedMyNotes
+					includeMyRenotes: (this as any).clientSettings.showMyRenotes,
+					includeRenotedMyNotes: (this as any).clientSettings.showRenotedMyNotes
 				}).then(notes => {
 					if (notes.length == fetchLimit + 1) {
 						notes.pop();
@@ -66,8 +66,8 @@ export default Vue.extend({
 				listId: this.list.id,
 				limit: fetchLimit + 1,
 				untilId: (this.$refs.timeline as any).tail().id,
-				includeMyRenotes: (this as any).os.i.clientSettings.showMyRenotes,
-				includeRenotedMyNotes: (this as any).os.i.clientSettings.showRenotedMyNotes
+				includeMyRenotes: (this as any).clientSettings.showMyRenotes,
+				includeRenotedMyNotes: (this as any).clientSettings.showRenotedMyNotes
 			}).then(notes => {
 				if (notes.length == fetchLimit + 1) {
 					notes.pop();
diff --git a/src/client/app/desktop/views/components/user-preview.vue b/src/client/app/desktop/views/components/user-preview.vue
index d0111d7dc..cc5e02139 100644
--- a/src/client/app/desktop/views/components/user-preview.vue
+++ b/src/client/app/desktop/views/components/user-preview.vue
@@ -2,9 +2,7 @@
 <div class="mk-user-preview">
 	<template v-if="u != null">
 		<div class="banner" :style="u.bannerUrl ? `background-image: url(${u.bannerUrl}?thumbnail&size=512)` : ''"></div>
-		<router-link class="avatar" :to="u | userPage">
-			<img :src="`${u.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
-		</router-link>
+		<mk-avatar class="avatar" :user="u" :disable-preview="true"/>
 		<div class="title">
 			<router-link class="name" :to="u | userPage">{{ u | userName }}</router-link>
 			<p class="username">@{{ u | acct }}</p>
@@ -111,14 +109,10 @@ root(isDark)
 		top 62px
 		left 13px
 		z-index 2
-
-		> img
-			display block
-			width 58px
-			height 58px
-			margin 0
-			border solid 3px isDark ? #282c37 : #fff
-			border-radius 8px
+		width 58px
+		height 58px
+		border solid 3px isDark ? #282c37 : #fff
+		border-radius 8px
 
 	> .title
 		display block
diff --git a/src/client/app/desktop/views/components/users-list.item.vue b/src/client/app/desktop/views/components/users-list.item.vue
index 005c9cd6d..dbad29517 100644
--- a/src/client/app/desktop/views/components/users-list.item.vue
+++ b/src/client/app/desktop/views/components/users-list.item.vue
@@ -1,8 +1,6 @@
 <template>
 <div class="root item">
-	<router-link class="avatar-anchor" :to="user | userPage" v-user-preview="user.id">
-		<img class="avatar" :src="`${user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
-	</router-link>
+	<mk-avatar class="avatar" :user="user"/>
 	<div class="main">
 		<header>
 			<router-link class="name" :to="user | userPage" v-user-preview="user.id">{{ user | userName }}</router-link>
@@ -35,18 +33,13 @@ export default Vue.extend({
 		display block
 		clear both
 
-	> .avatar-anchor
+	> .avatar
 		display block
 		float left
 		margin 0 16px 0 0
-
-		> .avatar
-			display block
-			width 58px
-			height 58px
-			margin 0
-			border-radius 8px
-			vertical-align bottom
+		width 58px
+		height 58px
+		border-radius 8px
 
 	> .main
 		float left
diff --git a/src/client/app/desktop/views/components/widget-container.vue b/src/client/app/desktop/views/components/widget-container.vue
index 2edba5a23..ab8327d39 100644
--- a/src/client/app/desktop/views/components/widget-container.vue
+++ b/src/client/app/desktop/views/components/widget-container.vue
@@ -24,8 +24,8 @@ export default Vue.extend({
 	computed: {
 		withGradient(): boolean {
 			return (this as any).os.isSignedIn
-				? (this as any).os.i.clientSettings.gradientWindowHeader != null
-					? (this as any).os.i.clientSettings.gradientWindowHeader
+				? (this as any).clientSettings.gradientWindowHeader != null
+					? (this as any).clientSettings.gradientWindowHeader
 					: false
 				: false;
 		}
diff --git a/src/client/app/desktop/views/components/window.vue b/src/client/app/desktop/views/components/window.vue
index c9820f869..91d1a9c2b 100644
--- a/src/client/app/desktop/views/components/window.vue
+++ b/src/client/app/desktop/views/components/window.vue
@@ -94,8 +94,8 @@ export default Vue.extend({
 		},
 		withGradient(): boolean {
 			return (this as any).os.isSignedIn
-				? (this as any).os.i.clientSettings.gradientWindowHeader != null
-					? (this as any).os.i.clientSettings.gradientWindowHeader
+				? (this as any).clientSettings.gradientWindowHeader != null
+					? (this as any).clientSettings.gradientWindowHeader
 					: false
 				: false;
 		}
diff --git a/src/client/app/desktop/views/pages/user-list.users.vue b/src/client/app/desktop/views/pages/user-list.users.vue
index 63070ed60..4236cdbb1 100644
--- a/src/client/app/desktop/views/pages/user-list.users.vue
+++ b/src/client/app/desktop/views/pages/user-list.users.vue
@@ -8,9 +8,7 @@
 			<p class="fetching" v-if="fetching">%fa:spinner .pulse .fw% %i18n:common.loading%<mk-ellipsis/></p>
 			<template v-else-if="users.length != 0">
 				<div class="user" v-for="_user in users">
-					<router-link class="avatar-anchor" :to="_user | userPage">
-						<img class="avatar" :src="`${_user.avatarUrl}?thumbnail&size=42`" alt="" v-user-preview="_user.id"/>
-					</router-link>
+					<mk-avatar class="avatar" :user="_user"/>
 					<div class="body">
 						<router-link class="name" :to="_user | userPage" v-user-preview="_user.id">{{ _user | userName }}</router-link>
 						<p class="username">@{{ _user | acct }}</p>
@@ -80,18 +78,13 @@ root(isDark)
 			display block
 			clear both
 
-		> .avatar-anchor
+		> .avatar
 			display block
 			float left
 			margin 0 12px 0 0
-
-			> .avatar
-				display block
-				width 42px
-				height 42px
-				margin 0
-				border-radius 8px
-				vertical-align bottom
+			width 42px
+			height 42px
+			border-radius 8px
 
 		> .body
 			float left
diff --git a/src/client/app/desktop/views/pages/user/user.friends.vue b/src/client/app/desktop/views/pages/user/user.friends.vue
index 161b08d1d..4af0f0bca 100644
--- a/src/client/app/desktop/views/pages/user/user.friends.vue
+++ b/src/client/app/desktop/views/pages/user/user.friends.vue
@@ -4,9 +4,7 @@
 	<p class="initializing" v-if="fetching">%fa:spinner .pulse .fw%%i18n:@loading%<mk-ellipsis/></p>
 	<template v-if="!fetching && users.length != 0">
 		<div class="user" v-for="friend in users">
-			<router-link class="avatar-anchor" :to="friend | userPage">
-				<img class="avatar" :src="`${friend.avatarUrl}?thumbnail&size=42`" alt="" v-user-preview="friend.id"/>
-			</router-link>
+			<mk-avatar class="avatar" :user="friend"/>
 			<div class="body">
 				<router-link class="name" :to="friend | userPage" v-user-preview="friend.id">{{ friend.name }}</router-link>
 				<p class="username">@{{ friend | acct }}</p>
@@ -82,18 +80,13 @@ export default Vue.extend({
 			display block
 			clear both
 
-		> .avatar-anchor
+		> .avatar
 			display block
 			float left
 			margin 0 12px 0 0
-
-			> .avatar
-				display block
-				width 42px
-				height 42px
-				margin 0
-				border-radius 8px
-				vertical-align bottom
+			width 42px
+			height 42px
+			border-radius 8px
 
 		> .body
 			float left
diff --git a/src/client/app/desktop/views/pages/user/user.header.vue b/src/client/app/desktop/views/pages/user/user.header.vue
index 99fe0b18d..629f6d87c 100644
--- a/src/client/app/desktop/views/pages/user/user.header.vue
+++ b/src/client/app/desktop/views/pages/user/user.header.vue
@@ -7,7 +7,7 @@
 		<div class="fade"></div>
 	</div>
 	<div class="container">
-		<img class="avatar" :src="`${user.avatarUrl}?thumbnail&size=150`" alt="avatar"/>
+		<mk-avatar class="avatar" :user="user" :disable-preview="true"/>
 		<div class="title">
 			<p class="name">{{ user | userName }}</p>
 			<p class="username">@{{ user | acct }}</p>
@@ -139,7 +139,6 @@ export default Vue.extend({
 			z-index 2
 			width 160px
 			height 160px
-			margin 0
 			border solid 3px #fff
 			border-radius 8px
 			box-shadow 1px 1px 3px rgba(#000, 0.2)
diff --git a/src/client/app/desktop/views/pages/welcome.vue b/src/client/app/desktop/views/pages/welcome.vue
index 223be8301..3d6765c97 100644
--- a/src/client/app/desktop/views/pages/welcome.vue
+++ b/src/client/app/desktop/views/pages/welcome.vue
@@ -8,9 +8,7 @@
 					<p>ようこそ! <b>Misskey</b>はTwitter風ミニブログSNSです。思ったことや皆と共有したいことを投稿しましょう。タイムラインを見れば、皆の関心事をすぐにチェックすることもできます。<a :href="aboutUrl">詳しく...</a></p>
 					<p><button class="signup" @click="signup">はじめる</button><button class="signin" @click="signin">ログイン</button></p>
 					<div class="users">
-						<router-link v-for="user in users" :key="user.id" class="avatar-anchor" :to="user | userPage" v-user-preview="user.id">
-							<img class="avatar" :src="`${user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
-						</router-link>
+						<mk-avatar class="avatar" :key="user.id" :user="user"/>
 					</div>
 				</div>
 				<div>
diff --git a/src/client/app/desktop/views/widgets/activity.vue b/src/client/app/desktop/views/widgets/activity.vue
index 0bdf4622a..1be87f590 100644
--- a/src/client/app/desktop/views/widgets/activity.vue
+++ b/src/client/app/desktop/views/widgets/activity.vue
@@ -22,9 +22,11 @@ export default define({
 			} else {
 				this.props.design++;
 			}
+			this.save();
 		},
 		viewChanged(view) {
 			this.props.view = view;
+			this.save();
 		}
 	}
 });
diff --git a/src/client/app/desktop/views/widgets/channel.vue b/src/client/app/desktop/views/widgets/channel.vue
index 600cdd531..d21aed40f 100644
--- a/src/client/app/desktop/views/widgets/channel.vue
+++ b/src/client/app/desktop/views/widgets/channel.vue
@@ -37,6 +37,7 @@ export default define({
 	methods: {
 		func() {
 			this.props.compact = !this.props.compact;
+			this.save();
 		},
 		settings() {
 			const id = window.prompt('チャンネルID');
diff --git a/src/client/app/desktop/views/widgets/messaging.vue b/src/client/app/desktop/views/widgets/messaging.vue
index b71db3e08..791d2ff1b 100644
--- a/src/client/app/desktop/views/widgets/messaging.vue
+++ b/src/client/app/desktop/views/widgets/messaging.vue
@@ -35,6 +35,7 @@ export default define({
 			} else {
 				this.props.design++;
 			}
+			this.save();
 		}
 	}
 });
diff --git a/src/client/app/desktop/views/widgets/notifications.vue b/src/client/app/desktop/views/widgets/notifications.vue
index 091b0d8b9..f75a09148 100644
--- a/src/client/app/desktop/views/widgets/notifications.vue
+++ b/src/client/app/desktop/views/widgets/notifications.vue
@@ -23,6 +23,7 @@ export default define({
 		},
 		func() {
 			this.props.compact = !this.props.compact;
+			this.save();
 		}
 	}
 });
diff --git a/src/client/app/desktop/views/widgets/polls.vue b/src/client/app/desktop/views/widgets/polls.vue
index 852bf2e3a..36fcc2063 100644
--- a/src/client/app/desktop/views/widgets/polls.vue
+++ b/src/client/app/desktop/views/widgets/polls.vue
@@ -39,6 +39,7 @@ export default define({
 	methods: {
 		func() {
 			this.props.compact = !this.props.compact;
+			this.save();
 		},
 		fetch() {
 			this.fetching = true;
diff --git a/src/client/app/desktop/views/widgets/post-form.vue b/src/client/app/desktop/views/widgets/post-form.vue
index 9c2d60f9b..69b21ad37 100644
--- a/src/client/app/desktop/views/widgets/post-form.vue
+++ b/src/client/app/desktop/views/widgets/post-form.vue
@@ -29,6 +29,7 @@ export default define({
 			} else {
 				this.props.design++;
 			}
+			this.save();
 		},
 		onKeydown(e) {
 			if ((e.which == 10 || e.which == 13) && (e.ctrlKey || e.metaKey)) this.post();
diff --git a/src/client/app/desktop/views/widgets/profile.vue b/src/client/app/desktop/views/widgets/profile.vue
index 7958b4829..48e7747b4 100644
--- a/src/client/app/desktop/views/widgets/profile.vue
+++ b/src/client/app/desktop/views/widgets/profile.vue
@@ -36,6 +36,7 @@ export default define({
 			} else {
 				this.props.design++;
 			}
+			this.save();
 		}
 	}
 });
diff --git a/src/client/app/desktop/views/widgets/timemachine.vue b/src/client/app/desktop/views/widgets/timemachine.vue
index 6db3b14c6..22a412040 100644
--- a/src/client/app/desktop/views/widgets/timemachine.vue
+++ b/src/client/app/desktop/views/widgets/timemachine.vue
@@ -22,6 +22,7 @@ export default define({
 			} else {
 				this.props.design++;
 			}
+			this.save();
 		}
 	}
 });
diff --git a/src/client/app/desktop/views/widgets/trends.vue b/src/client/app/desktop/views/widgets/trends.vue
index 08fa7f802..c33bf2f2f 100644
--- a/src/client/app/desktop/views/widgets/trends.vue
+++ b/src/client/app/desktop/views/widgets/trends.vue
@@ -38,6 +38,7 @@ export default define({
 	methods: {
 		func() {
 			this.props.compact = !this.props.compact;
+			this.save();
 		},
 		fetch() {
 			this.fetching = true;
diff --git a/src/client/app/desktop/views/widgets/users.vue b/src/client/app/desktop/views/widgets/users.vue
index 6e326115b..328fa5669 100644
--- a/src/client/app/desktop/views/widgets/users.vue
+++ b/src/client/app/desktop/views/widgets/users.vue
@@ -8,9 +8,7 @@
 			<p class="fetching" v-if="fetching">%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p>
 			<template v-else-if="users.length != 0">
 				<div class="user" v-for="_user in users">
-					<router-link class="avatar-anchor" :to="_user | userPage">
-						<img class="avatar" :src="`${_user.avatarUrl}?thumbnail&size=42`" alt="" v-user-preview="_user.id"/>
-					</router-link>
+					<mk-avatar class="avatar" :user="_user"/>
 					<div class="body">
 						<router-link class="name" :to="_user | userPage" v-user-preview="_user.id">{{ _user | userName }}</router-link>
 						<p class="username">@{{ _user | acct }}</p>
@@ -48,6 +46,7 @@ export default define({
 	methods: {
 		func() {
 			this.props.compact = !this.props.compact;
+			this.save();
 		},
 		fetch() {
 			this.fetching = true;
@@ -88,18 +87,13 @@ root(isDark)
 				display block
 				clear both
 
-			> .avatar-anchor
+			> .avatar
 				display block
 				float left
 				margin 0 12px 0 0
-
-				> .avatar
-					display block
-					width 42px
-					height 42px
-					margin 0
-					border-radius 8px
-					vertical-align bottom
+				width 42px
+				height 42px
+				border-radius 8px
 
 			> .body
 				float left
diff --git a/src/client/app/init.ts b/src/client/app/init.ts
index 26f5328d7..6f7ce0260 100644
--- a/src/client/app/init.ts
+++ b/src/client/app/init.ts
@@ -3,7 +3,7 @@
  */
 
 import Vue from 'vue';
-import Vuex from 'vuex';
+import Vuex, { mapState } from 'vuex';
 import VueRouter from 'vue-router';
 import VModal from 'vue-js-modal';
 import * as TreeView from 'vue-json-tree-view';
@@ -41,17 +41,6 @@ require('./common/views/widgets');
 // Register global filters
 require('./common/views/filters');
 
-const store = new Vuex.Store({
-	state: {
-		uiHeaderHeight: 0
-	},
-	mutations: {
-		setUiHeaderHeight(state, height) {
-			state.uiHeaderHeight = height;
-		}
-	}
-});
-
 Vue.mixin({
 	destroyed(this: any) {
 		if (this.$el.parentNode) {
@@ -159,20 +148,15 @@ export default (callback: (launch: (router: VueRouter, api?: (os: MiOS) => API)
 						api: os.api,
 						apis: os.apis
 					};
-				}
+				},
+				computed: mapState({
+					clientSettings: state => state.settings.data
+				})
 			});
 
 			const app = new Vue({
-				store,
+				store: os.store,
 				router,
-				created() {
-					this.$watch('os.i', i => {
-						// キャッシュ更新
-						localStorage.setItem('me', JSON.stringify(i));
-					}, {
-						deep: true
-					});
-				},
 				render: createEl => createEl(App)
 			});
 
diff --git a/src/client/app/mobile/views/components/note-detail.sub.vue b/src/client/app/mobile/views/components/note-detail.sub.vue
index 683b5ffd1..34ff06db3 100644
--- a/src/client/app/mobile/views/components/note-detail.sub.vue
+++ b/src/client/app/mobile/views/components/note-detail.sub.vue
@@ -1,8 +1,6 @@
 <template>
 <div class="root sub">
-	<router-link class="avatar-anchor" :to="note.user | userPage">
-		<img class="avatar" :src="`${note.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
-	</router-link>
+	<mk-avatar class="avatar" :user="note.user"/>
 	<div class="main">
 		<header>
 			<router-link class="name" :to="note.user | userPage">{{ note.user | userName }}</router-link>
@@ -43,18 +41,13 @@ root(isDark)
 		display block
 		clear both
 
-	> .avatar-anchor
+	> .avatar
 		display block
 		float left
 		margin 0 12px 0 0
-
-		> .avatar
-			display block
-			width 48px
-			height 48px
-			margin 0
-			border-radius 8px
-			vertical-align bottom
+		width 48px
+		height 48px
+		border-radius 8px
 
 	> .main
 		float left
diff --git a/src/client/app/mobile/views/components/note-detail.vue b/src/client/app/mobile/views/components/note-detail.vue
index 36c3f30c6..b9bd9fe49 100644
--- a/src/client/app/mobile/views/components/note-detail.vue
+++ b/src/client/app/mobile/views/components/note-detail.vue
@@ -17,17 +17,12 @@
 	</div>
 	<div class="renote" v-if="isRenote">
 		<p>
-			<router-link class="avatar-anchor" :to="note.user | userPage">
-				<img class="avatar" :src="`${note.user.avatarUrl}?thumbnail&size=32`" alt="avatar"/>
-			</router-link>
-			%fa:retweet%<router-link class="name" :to="note.user | userPage">{{ note.user | userName }}</router-link>がRenote
+			<mk-avatar class="avatar" :user="note.user"/>%fa:retweet%<router-link class="name" :to="note.user | userPage">{{ note.user | userName }}</router-link>がRenote
 		</p>
 	</div>
 	<article>
 		<header>
-			<router-link class="avatar-anchor" :to="p.user | userPage">
-				<img class="avatar" :src="`${p.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
-			</router-link>
+			<mk-avatar class="avatar" :user="p.user"/>
 			<div>
 				<router-link class="name" :to="p.user | userPage">{{ p.user | userName }}</router-link>
 				<span class="username">@{{ p.user | acct }}</span>
@@ -152,7 +147,7 @@ export default Vue.extend({
 
 		// Draw map
 		if (this.p.geo) {
-			const shouldShowMap = (this as any).os.isSignedIn ? (this as any).os.i.clientSettings.showMaps : true;
+			const shouldShowMap = (this as any).os.isSignedIn ? (this as any).clientSettings.showMaps : true;
 			if (shouldShowMap) {
 				(this as any).os.getGoogleMaps().then(maps => {
 					const uluru = new maps.LatLng(this.p.geo.coordinates[1], this.p.geo.coordinates[0]);
@@ -262,17 +257,12 @@ root(isDark)
 			margin 0
 			padding 16px 32px
 
-			.avatar-anchor
+			.avatar
 				display inline-block
-
-				.avatar
-					vertical-align bottom
-					min-width 28px
-					min-height 28px
-					max-width 28px
-					max-height 28px
-					margin 0 8px 0 0
-					border-radius 6px
+				width 28px
+				height 28px
+				margin 0 8px 0 0
+				border-radius 6px
 
 			[data-fa]
 				margin-right 4px
@@ -301,21 +291,16 @@ root(isDark)
 			display flex
 			line-height 1.1em
 
-			> .avatar-anchor
+			> .avatar
 				display block
-				padding 0 12px 0 0
+				margin 0 12px 0 0
+				width 54px
+				height 54px
+				border-radius 8px
 
-				> .avatar
-					display block
-					width 54px
-					height 54px
-					margin 0
-					border-radius 8px
-					vertical-align bottom
-
-					@media (min-width 500px)
-						width 60px
-						height 60px
+				@media (min-width 500px)
+					width 60px
+					height 60px
 
 			> div
 
diff --git a/src/client/app/mobile/views/components/note-preview.vue b/src/client/app/mobile/views/components/note-preview.vue
index 41244cc75..e9fe9b1bd 100644
--- a/src/client/app/mobile/views/components/note-preview.vue
+++ b/src/client/app/mobile/views/components/note-preview.vue
@@ -1,8 +1,6 @@
 <template>
 <div class="mk-note-preview">
-	<router-link class="avatar-anchor" :to="note.user | userPage">
-		<img class="avatar" :src="`${note.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
-	</router-link>
+	<mk-avatar class="avatar" :user="note.user"/>
 	<div class="main">
 		<header>
 			<router-link class="name" :to="note.user | userPage">{{ note.user | userName }}</router-link>
@@ -37,18 +35,13 @@ root(isDark)
 		display block
 		clear both
 
-	> .avatar-anchor
+	> .avatar
 		display block
 		float left
 		margin 0 12px 0 0
-
-		> .avatar
-			display block
-			width 48px
-			height 48px
-			margin 0
-			border-radius 8px
-			vertical-align bottom
+		width 48px
+		height 48px
+		border-radius 8px
 
 	> .main
 		float left
diff --git a/src/client/app/mobile/views/components/note.sub.vue b/src/client/app/mobile/views/components/note.sub.vue
index 01f02bdb5..251b377fb 100644
--- a/src/client/app/mobile/views/components/note.sub.vue
+++ b/src/client/app/mobile/views/components/note.sub.vue
@@ -1,8 +1,6 @@
 <template>
 <div class="sub">
-	<router-link class="avatar-anchor" :to="note.user | userPage">
-		<img class="avatar" :src="`${note.user.avatarUrl}?thumbnail&size=96`" alt="avatar"/>
-	</router-link>
+	<mk-avatar class="avatar" :user="note.user"/>
 	<div class="main">
 		<header>
 			<router-link class="name" :to="note.user | userPage">{{ note.user | userName }}</router-link>
@@ -49,25 +47,18 @@ root(isDark)
 		display block
 		clear both
 
-	> .avatar-anchor
+	> .avatar
 		display block
 		float left
 		margin 0 10px 0 0
+		width 44px
+		height 44px
+		border-radius 8px
 
 		@media (min-width 500px)
 			margin-right 16px
-
-		> .avatar
-			display block
-			width 44px
-			height 44px
-			margin 0
-			border-radius 8px
-			vertical-align bottom
-
-			@media (min-width 500px)
-				width 52px
-				height 52px
+			width 52px
+			height 52px
 
 	> .main
 		float left
diff --git a/src/client/app/mobile/views/components/note.vue b/src/client/app/mobile/views/components/note.vue
index 07e18544d..f3aab49bb 100644
--- a/src/client/app/mobile/views/components/note.vue
+++ b/src/client/app/mobile/views/components/note.vue
@@ -1,12 +1,10 @@
 <template>
 <div class="note" :class="{ renote: isRenote }">
-	<div class="reply-to" v-if="p.reply && (!os.isSignedIn || os.i.clientSettings.showReplyTarget)">
+	<div class="reply-to" v-if="p.reply && (!os.isSignedIn || clientSettings.showReplyTarget)">
 		<x-sub :note="p.reply"/>
 	</div>
 	<div class="renote" v-if="isRenote">
-		<router-link class="avatar-anchor" :to="note.user | userPage">
-			<img class="avatar" :src="`${note.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
-		</router-link>
+		<mk-avatar class="avatar" :user="note.user"/>
 		%fa:retweet%
 		<span>{{ '%i18n:!@reposted-by%'.substr(0, '%i18n:!@reposted-by%'.indexOf('{')) }}</span>
 		<router-link class="name" :to="note.user | userPage">{{ note.user | userName }}</router-link>
@@ -14,9 +12,7 @@
 		<mk-time :time="note.createdAt"/>
 	</div>
 	<article>
-		<router-link class="avatar-anchor" :to="p.user | userPage">
-			<img class="avatar" :src="`${p.user.avatarUrl}?thumbnail&size=96`" alt="avatar"/>
-		</router-link>
+		<mk-avatar class="avatar" :user="p.user"/>
 		<div class="main">
 			<header>
 				<router-link class="name" :to="p.user | userPage">{{ p.user | userName }}</router-link>
@@ -154,7 +150,7 @@ export default Vue.extend({
 
 		// Draw map
 		if (this.p.geo) {
-			const shouldShowMap = (this as any).os.isSignedIn ? (this as any).os.i.clientSettings.showMaps : true;
+			const shouldShowMap = (this as any).os.isSignedIn ? (this as any).clientSettings.showMaps : true;
 			if (shouldShowMap) {
 				(this as any).os.getGoogleMaps().then(maps => {
 					const uluru = new maps.LatLng(this.p.geo.coordinates[1], this.p.geo.coordinates[0]);
@@ -268,15 +264,12 @@ root(isDark)
 		@media (min-width 600px)
 			padding 16px 32px
 
-		.avatar-anchor
+		.avatar
 			display inline-block
-
-			.avatar
-				vertical-align bottom
-				width 28px
-				height 28px
-				margin 0 8px 0 0
-				border-radius 6px
+			width 28px
+			height 28px
+			margin 0 8px 0 0
+			border-radius 6px
 
 		[data-fa]
 			margin-right 4px
@@ -314,29 +307,22 @@ root(isDark)
 			display block
 			clear both
 
-		> .avatar-anchor
+		> .avatar
 			display block
 			float left
 			margin 0 10px 8px 0
+			width 48px
+			height 48px
+			border-radius 6px
 			//position -webkit-sticky
 			//position sticky
 			//top 62px
 
 			@media (min-width 500px)
 				margin-right 16px
-
-			> .avatar
-				display block
-				width 48px
-				height 48px
-				margin 0
-				border-radius 6px
-				vertical-align bottom
-
-				@media (min-width 500px)
-					width 58px
-					height 58px
-					border-radius 8px
+				width 58px
+				height 58px
+				border-radius 8px
 
 		> .main
 			float left
diff --git a/src/client/app/mobile/views/components/notes.vue b/src/client/app/mobile/views/components/notes.vue
index e12dc1d04..9461fe6fe 100644
--- a/src/client/app/mobile/views/components/notes.vue
+++ b/src/client/app/mobile/views/components/notes.vue
@@ -116,13 +116,13 @@ export default Vue.extend({
 			const isMyNote = note.userId == (this as any).os.i.id;
 			const isPureRenote = note.renoteId != null && note.text == null && note.mediaIds.length == 0 && note.poll == null;
 
-			if ((this as any).os.i.clientSettings.showMyRenotes === false) {
+			if ((this as any).clientSettings.showMyRenotes === false) {
 				if (isMyNote && isPureRenote) {
 					return;
 				}
 			}
 
-			if ((this as any).os.i.clientSettings.showRenotedMyNotes === false) {
+			if ((this as any).clientSettings.showRenotedMyNotes === false) {
 				if (isPureRenote && (note.renote.userId == (this as any).os.i.id)) {
 					return;
 				}
@@ -187,7 +187,7 @@ export default Vue.extend({
 				this.clearNotification();
 			}
 
-			if ((this as any).os.i.clientSettings.fetchOnScroll !== false) {
+			if ((this as any).clientSettings.fetchOnScroll !== false) {
 				const current = window.scrollY + window.innerHeight;
 				if (current > document.body.offsetHeight - 8) this.loadMore();
 			}
diff --git a/src/client/app/mobile/views/components/notification.vue b/src/client/app/mobile/views/components/notification.vue
index a4e6b027e..13ca95075 100644
--- a/src/client/app/mobile/views/components/notification.vue
+++ b/src/client/app/mobile/views/components/notification.vue
@@ -1,9 +1,7 @@
 <template>
 <div class="mk-notification">
 	<div class="notification reaction" v-if="notification.type == 'reaction'">
-		<router-link class="avatar-anchor" :to="notification.user | userPage">
-			<img class="avatar" :src="`${notification.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
-		</router-link>
+		<mk-avatar class="avatar" :user="notification.user"/>
 		<div>
 			<header>
 				<mk-reaction-icon :reaction="notification.reaction"/>
@@ -18,9 +16,7 @@
 	</div>
 
 	<div class="notification renote" v-if="notification.type == 'renote'">
-		<router-link class="avatar-anchor" :to="notification.user | userPage">
-			<img class="avatar" :src="`${notification.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
-		</router-link>
+		<mk-avatar class="avatar" :user="notification.user"/>
 		<div>
 			<header>
 				%fa:retweet%
@@ -34,9 +30,7 @@
 	</div>
 
 	<div class="notification follow" v-if="notification.type == 'follow'">
-		<router-link class="avatar-anchor" :to="notification.user | userPage">
-			<img class="avatar" :src="`${notification.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
-		</router-link>
+		<mk-avatar class="avatar" :user="notification.user"/>
 		<div>
 			<header>
 				%fa:user-plus%
@@ -47,9 +41,7 @@
 	</div>
 
 	<div class="notification poll_vote" v-if="notification.type == 'poll_vote'">
-		<router-link class="avatar-anchor" :to="notification.user | userPage">
-			<img class="avatar" :src="`${notification.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
-		</router-link>
+		<mk-avatar class="avatar" :user="notification.user"/>
 		<div>
 			<header>
 				%fa:chart-pie%
@@ -111,18 +103,16 @@ root(isDark)
 			display block
 			clear both
 
-		> .avatar-anchor
+		> .avatar
 			display block
 			float left
+			width 36px
+			height 36px
+			border-radius 6px
 
-			img
-				width 36px
-				height 36px
-				border-radius 6px
-
-				@media (min-width 500px)
-					width 42px
-					height 42px
+			@media (min-width 500px)
+				width 42px
+				height 42px
 
 		> div
 			float right
diff --git a/src/client/app/mobile/views/components/post-form.vue b/src/client/app/mobile/views/components/post-form.vue
index ec1611979..3f890223a 100644
--- a/src/client/app/mobile/views/components/post-form.vue
+++ b/src/client/app/mobile/views/components/post-form.vue
@@ -166,7 +166,7 @@ export default Vue.extend({
 
 		post() {
 			this.posting = true;
-			const viaMobile = (this as any).os.i.clientSettings.disableViaMobile !== true;
+			const viaMobile = (this as any).clientSettings.disableViaMobile !== true;
 			(this as any).api('notes/create', {
 				text: this.text == '' ? undefined : this.text,
 				mediaIds: this.files.length > 0 ? this.files.map(f => f.id) : undefined,
diff --git a/src/client/app/mobile/views/components/user-list-timeline.vue b/src/client/app/mobile/views/components/user-list-timeline.vue
index ee983a969..ccdf8f64d 100644
--- a/src/client/app/mobile/views/components/user-list-timeline.vue
+++ b/src/client/app/mobile/views/components/user-list-timeline.vue
@@ -46,8 +46,8 @@ export default Vue.extend({
 				(this as any).api('notes/user-list-timeline', {
 					listId: this.list.id,
 					limit: fetchLimit + 1,
-					includeMyRenotes: (this as any).os.i.clientSettings.showMyRenotes,
-					includeRenotedMyNotes: (this as any).os.i.clientSettings.showRenotedMyNotes
+					includeMyRenotes: (this as any).clientSettings.showMyRenotes,
+					includeRenotedMyNotes: (this as any).clientSettings.showRenotedMyNotes
 				}).then(notes => {
 					if (notes.length == fetchLimit + 1) {
 						notes.pop();
@@ -66,8 +66,8 @@ export default Vue.extend({
 				listId: this.list.id,
 				limit: fetchLimit + 1,
 				untilId: (this.$refs.timeline as any).tail().id,
-				includeMyRenotes: (this as any).os.i.clientSettings.showMyRenotes,
-				includeRenotedMyNotes: (this as any).os.i.clientSettings.showRenotedMyNotes
+				includeMyRenotes: (this as any).clientSettings.showMyRenotes,
+				includeRenotedMyNotes: (this as any).clientSettings.showRenotedMyNotes
 			}).then(notes => {
 				if (notes.length == fetchLimit + 1) {
 					notes.pop();
diff --git a/src/client/app/mobile/views/components/user-preview.vue b/src/client/app/mobile/views/components/user-preview.vue
index 23a83b5e3..d25836091 100644
--- a/src/client/app/mobile/views/components/user-preview.vue
+++ b/src/client/app/mobile/views/components/user-preview.vue
@@ -1,8 +1,6 @@
 <template>
 <div class="mk-user-preview">
-	<router-link class="avatar-anchor" :to="user | userPage">
-		<img class="avatar" :src="`${user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
-	</router-link>
+	<mk-avatar class="avatar" :user="user"/>
 	<div class="main">
 		<header>
 			<router-link class="name" :to="user | userPage">{{ user | userName }}</router-link>
@@ -40,26 +38,19 @@ export default Vue.extend({
 		display block
 		clear both
 
-	> .avatar-anchor
+	> .avatar
 		display block
 		float left
 		margin 0 10px 0 0
+		width 48px
+		height 48px
+		border-radius 6px
 
 		@media (min-width 500px)
 			margin-right 16px
-
-		> .avatar
-			display block
-			width 48px
-			height 48px
-			margin 0
-			border-radius 6px
-			vertical-align bottom
-
-			@media (min-width 500px)
-				width 58px
-				height 58px
-				border-radius 8px
+			width 58px
+			height 58px
+			border-radius 8px
 
 	> .main
 		float left
diff --git a/src/client/app/mobile/views/pages/dashboard.vue b/src/client/app/mobile/views/pages/dashboard.vue
index 53fe33ee8..a5ca6cb4a 100644
--- a/src/client/app/mobile/views/pages/dashboard.vue
+++ b/src/client/app/mobile/views/pages/dashboard.vue
@@ -64,8 +64,8 @@ export default Vue.extend({
 		};
 	},
 	created() {
-		if ((this as any).os.i.clientSettings.mobileHome == null) {
-			Vue.set((this as any).os.i.clientSettings, 'mobileHome', [{
+		if ((this as any).clientSettings.mobileHome == null) {
+			Vue.set((this as any).clientSettings, 'mobileHome', [{
 				name: 'calendar',
 				id: 'a', data: {}
 			}, {
@@ -87,14 +87,14 @@ export default Vue.extend({
 				name: 'version',
 				id: 'g', data: {}
 			}]);
-			this.widgets = (this as any).os.i.clientSettings.mobileHome;
+			this.widgets = (this as any).clientSettings.mobileHome;
 			this.saveHome();
 		} else {
-			this.widgets = (this as any).os.i.clientSettings.mobileHome;
+			this.widgets = (this as any).clientSettings.mobileHome;
 		}
 
-		this.$watch('os.i.clientSettings', i => {
-			this.widgets = (this as any).os.i.clientSettings.mobileHome;
+		this.$watch('clientSettings', i => {
+			this.widgets = (this as any).clientSettings.mobileHome;
 		}, {
 			deep: true
 		});
@@ -107,15 +107,15 @@ export default Vue.extend({
 	methods: {
 		onHomeUpdated(data) {
 			if (data.home) {
-				(this as any).os.i.clientSettings.mobileHome = data.home;
+				(this as any).clientSettings.mobileHome = data.home;
 				this.widgets = data.home;
 			} else {
-				const w = (this as any).os.i.clientSettings.mobileHome.find(w => w.id == data.id);
+				const w = (this as any).clientSettings.mobileHome.find(w => w.id == data.id);
 				if (w != null) {
 					w.data = data.data;
 					this.$refs[w.id][0].preventSave = true;
 					this.$refs[w.id][0].props = w.data;
-					this.widgets = (this as any).os.i.clientSettings.mobileHome;
+					this.widgets = (this as any).clientSettings.mobileHome;
 				}
 			}
 		},
@@ -144,7 +144,7 @@ export default Vue.extend({
 			this.saveHome();
 		},
 		saveHome() {
-			(this as any).os.i.clientSettings.mobileHome = this.widgets;
+			(this as any).clientSettings.mobileHome = this.widgets;
 			(this as any).api('i/update_mobile_home', {
 				home: this.widgets
 			});
diff --git a/src/client/app/mobile/views/pages/home.timeline.vue b/src/client/app/mobile/views/pages/home.timeline.vue
index 5f4bd6dcd..4c1c344db 100644
--- a/src/client/app/mobile/views/pages/home.timeline.vue
+++ b/src/client/app/mobile/views/pages/home.timeline.vue
@@ -92,8 +92,8 @@ export default Vue.extend({
 				(this as any).api(this.endpoint, {
 					limit: fetchLimit + 1,
 					untilDate: this.date ? this.date.getTime() : undefined,
-					includeMyRenotes: (this as any).os.i.clientSettings.showMyRenotes,
-					includeRenotedMyNotes: (this as any).os.i.clientSettings.showRenotedMyNotes
+					includeMyRenotes: (this as any).clientSettings.showMyRenotes,
+					includeRenotedMyNotes: (this as any).clientSettings.showRenotedMyNotes
 				}).then(notes => {
 					if (notes.length == fetchLimit + 1) {
 						notes.pop();
@@ -114,8 +114,8 @@ export default Vue.extend({
 			(this as any).api(this.endpoint, {
 				limit: fetchLimit + 1,
 				untilId: (this.$refs.timeline as any).tail().id,
-				includeMyRenotes: (this as any).os.i.clientSettings.showMyRenotes,
-				includeRenotedMyNotes: (this as any).os.i.clientSettings.showRenotedMyNotes
+				includeMyRenotes: (this as any).clientSettings.showMyRenotes,
+				includeRenotedMyNotes: (this as any).clientSettings.showRenotedMyNotes
 			}).then(notes => {
 				if (notes.length == fetchLimit + 1) {
 					notes.pop();
diff --git a/src/client/app/mobile/views/pages/welcome.vue b/src/client/app/mobile/views/pages/welcome.vue
index 485996870..4d236d7aa 100644
--- a/src/client/app/mobile/views/pages/welcome.vue
+++ b/src/client/app/mobile/views/pages/welcome.vue
@@ -22,9 +22,7 @@
 			<mk-welcome-timeline/>
 		</div>
 		<div class="users">
-			<router-link v-for="user in users" :key="user.id" class="avatar-anchor" :to="`/@${user.username}`">
-				<img class="avatar" :src="`${user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
-			</router-link>
+			<mk-avatar class="avatar" :key="user.id" :user="user"/>
 		</div>
 		<footer>
 			<small>{{ copyright }}</small>
diff --git a/src/client/app/mobile/views/widgets/activity.vue b/src/client/app/mobile/views/widgets/activity.vue
index 48dcafb3e..7763be41f 100644
--- a/src/client/app/mobile/views/widgets/activity.vue
+++ b/src/client/app/mobile/views/widgets/activity.vue
@@ -21,6 +21,7 @@ export default define({
 	methods: {
 		func() {
 			this.props.compact = !this.props.compact;
+			this.save();
 		}
 	}
 });
diff --git a/src/client/app/store.ts b/src/client/app/store.ts
new file mode 100644
index 000000000..706fd6555
--- /dev/null
+++ b/src/client/app/store.ts
@@ -0,0 +1,90 @@
+import Vuex from 'vuex';
+import MiOS from './common/mios';
+
+const defaultSettings = {
+	home: [],
+	fetchOnScroll: true,
+	showMaps: true,
+	showPostFormOnTopOfTl: false,
+	circleIcons: true,
+	gradientWindowHeader: false,
+	showReplyTarget: true,
+	showMyRenotes: true,
+	showRenotedMyNotes: true
+};
+
+export default (os: MiOS) => new Vuex.Store({
+	plugins: [store => {
+		store.subscribe((mutation, state) => {
+			if (mutation.type.startsWith('settings/')) {
+				localStorage.setItem('settings', JSON.stringify(state.settings.data));
+			}
+		});
+	}],
+
+	state: {
+		uiHeaderHeight: 0
+	},
+
+	mutations: {
+		setUiHeaderHeight(state, height) {
+			state.uiHeaderHeight = height;
+		}
+	},
+
+	modules: {
+		settings: {
+			namespaced: true,
+
+			state: {
+				data: defaultSettings
+			},
+
+			mutations: {
+				init(state, settings) {
+					state.data = settings;
+				},
+
+				set(state, x: { key: string; value: any }) {
+					state.data[x.key] = x.value;
+				},
+
+				setHome(state, data) {
+					state.data.home = data;
+				},
+
+				setHomeWidget(state, x) {
+					const w = state.data.home.find(w => w.id == x.id);
+					if (w) {
+						w.data = x.data;
+					}
+				},
+
+				addHomeWidget(state, widget) {
+					state.data.home.unshift(widget);
+				}
+			},
+
+			actions: {
+				set(ctx, x) {
+					ctx.commit('set', x);
+
+					if (os.isSignedIn) {
+						os.api('i/update_client_setting', {
+							name: x.key,
+							value: x.value
+						});
+					}
+				},
+
+				addHomeWidget(ctx, widget) {
+					ctx.commit('addHomeWidget', widget);
+
+					os.api('i/update_home', {
+						home: ctx.state.data.home
+					});
+				}
+			}
+		}
+	}
+});
diff --git a/src/server/api/endpoints/i/update_client_setting.ts b/src/server/api/endpoints/i/update_client_setting.ts
index 2edc2104d..278eb1cbe 100644
--- a/src/server/api/endpoints/i/update_client_setting.ts
+++ b/src/server/api/endpoints/i/update_client_setting.ts
@@ -24,16 +24,11 @@ module.exports = async (params, user) => new Promise(async (res, rej) => {
 		$set: x
 	});
 
-	// Serialize
-	user.clientSettings[name] = value;
-	const iObj = await pack(user, user, {
-		detail: true,
-		includeSecrets: true
+	res();
+
+	// Publish event
+	event(user._id, 'clientSettingUpdated', {
+		key: name,
+		value
 	});
-
-	// Send response
-	res(iObj);
-
-	// Publish i updated event
-	event(user._id, 'i_updated', iObj);
 });
diff --git a/src/server/index.ts b/src/server/index.ts
index 594f40c22..4c5aab9a2 100644
--- a/src/server/index.ts
+++ b/src/server/index.ts
@@ -26,9 +26,9 @@ if (process.env.NODE_ENV != 'production') {
 	app.use(logger());
 
 	// Delay
-	app.use(slow({
-		delay: 1000
-	}));
+	//app.use(slow({
+	//	delay: 1000
+	//}));
 }
 
 // Compress response