diff --git a/res/css/structures/_LeftPanel2.scss b/res/css/structures/_LeftPanel2.scss
index 6be5674d7f..935511b160 100644
--- a/res/css/structures/_LeftPanel2.scss
+++ b/res/css/structures/_LeftPanel2.scss
@@ -54,7 +54,11 @@ $tagPanelWidth: 70px; // only applies in this file, used for calculations
         flex-direction: column;
 
         .mx_LeftPanel2_userHeader {
-            padding: 12px 12px 20px; // 12px top, 12px sides, 20px bottom
+            /* 12px top, 12px sides, 20px bottom (using 13px bottom to account
+             * for internal whitespace in the breadcrumbs)
+             */
+            padding: 12px 12px 13px;
+            flex-shrink: 0; // to convince safari's layout engine the flexbox is fine
 
             // Create another flexbox column for the rows to stack within
             display: flex;
@@ -72,7 +76,20 @@ $tagPanelWidth: 70px; // only applies in this file, used for calculations
                 width: 100%;
                 overflow-y: hidden;
                 overflow-x: scroll;
-                margin-top: 8px;
+                margin-top: 20px;
+                padding-bottom: 2px;
+
+                &.mx_IndicatorScrollbar_leftOverflow {
+                    mask-image: linear-gradient(90deg, transparent, black 10%);
+                }
+
+                &.mx_IndicatorScrollbar_rightOverflow {
+                    mask-image: linear-gradient(90deg, black, black 90%, transparent);
+                }
+
+                &.mx_IndicatorScrollbar_rightOverflow.mx_IndicatorScrollbar_leftOverflow {
+                    mask-image: linear-gradient(90deg, transparent, black 10%, black 90%, transparent);
+                }
             }
         }
 
@@ -80,6 +97,8 @@ $tagPanelWidth: 70px; // only applies in this file, used for calculations
             margin-left: 12px;
             margin-right: 12px;
 
+            flex-shrink: 0; // to convince safari's layout engine the flexbox is fine
+
             // Create a flexbox to organize the inputs
             display: flex;
             align-items: center;
diff --git a/res/css/views/rooms/_RoomSublist2.scss b/res/css/views/rooms/_RoomSublist2.scss
index 7e85c8583d..527f50c5bc 100644
--- a/res/css/views/rooms/_RoomSublist2.scss
+++ b/res/css/views/rooms/_RoomSublist2.scss
@@ -24,6 +24,8 @@ limitations under the License.
     margin-left: 8px;
     width: 100%;
 
+    flex-shrink: 0; // to convince safari's layout engine the flexbox is fine
+
     .mx_RoomSublist2_headerContainer {
         // Create a flexbox to make alignment easy
         display: flex;
diff --git a/src/@types/global.d.ts b/src/@types/global.d.ts
index fc52296d8b..3f970ea8c3 100644
--- a/src/@types/global.d.ts
+++ b/src/@types/global.d.ts
@@ -37,6 +37,9 @@ declare global {
         mx_RoomListStore2: RoomListStore2;
         mx_RoomListLayoutStore: RoomListLayoutStore;
         mxPlatformPeg: PlatformPeg;
+
+        // TODO: Remove flag before launch: https://github.com/vector-im/riot-web/issues/14231
+        mx_QuietRoomListLogging: boolean;
     }
 
     // workaround for https://github.com/microsoft/TypeScript/issues/30933
diff --git a/src/components/views/rooms/RoomList2.tsx b/src/components/views/rooms/RoomList2.tsx
index 710af076cd..67787963a3 100644
--- a/src/components/views/rooms/RoomList2.tsx
+++ b/src/components/views/rooms/RoomList2.tsx
@@ -219,7 +219,10 @@ export default class RoomList2 extends React.Component<IProps, IState> {
 
     private updateLists = () => {
         const newLists = RoomListStore.instance.orderedLists;
-        console.log("new lists", newLists);
+        if (!window.mx_QuietRoomListLogging) {
+            // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
+            console.log("new lists", newLists);
+        }
 
         this.setState({sublists: newLists}, () => {
             this.props.onResize();
diff --git a/src/components/views/rooms/RoomTile2.tsx b/src/components/views/rooms/RoomTile2.tsx
index 54395fa2b7..ed188e996b 100644
--- a/src/components/views/rooms/RoomTile2.tsx
+++ b/src/components/views/rooms/RoomTile2.tsx
@@ -256,7 +256,7 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
                 0
             ));
         } else {
-            console.log(`Unexpected tag ${tagId} applied to ${this.props.room.room_id}`);
+            console.warn(`Unexpected tag ${tagId} applied to ${this.props.room.room_id}`);
         }
 
         if ((ev as React.KeyboardEvent).key === Key.ENTER) {
diff --git a/src/stores/room-list/RoomListStore2.ts b/src/stores/room-list/RoomListStore2.ts
index bb3cd63ceb..bbc2e7f478 100644
--- a/src/stores/room-list/RoomListStore2.ts
+++ b/src/stores/room-list/RoomListStore2.ts
@@ -33,6 +33,7 @@ import { EffectiveMembership, getEffectiveMembership } from "./membership";
 import { ListLayout } from "./ListLayout";
 import { isNullOrUndefined } from "matrix-js-sdk/src/utils";
 import RoomListLayoutStore from "./RoomListLayoutStore";
+import { MarkedExecution } from "../../utils/MarkedExecution";
 
 interface IState {
     tagsEnabled?: boolean;
@@ -51,7 +52,7 @@ export class RoomListStore2 extends AsyncStore<ActionPayload> {
     private algorithm = new Algorithm();
     private filterConditions: IFilterCondition[] = [];
     private tagWatcher = new TagWatcher(this);
-    private layoutMap: Map<TagID, ListLayout> = new Map<TagID, ListLayout>();
+    private updateFn = new MarkedExecution(() => this.emit(LISTS_UPDATE_EVENT));
 
     private readonly watchedSettings = [
         'feature_custom_tags',
@@ -62,7 +63,7 @@ export class RoomListStore2 extends AsyncStore<ActionPayload> {
 
         this.checkEnabled();
         for (const settingName of this.watchedSettings) SettingsStore.monitorSetting(settingName, null);
-        RoomViewStore.addListener(this.onRVSUpdate);
+        RoomViewStore.addListener(() => this.handleRVSUpdate({}));
         this.algorithm.on(LIST_UPDATED_EVENT, this.onAlgorithmListUpdated);
     }
 
@@ -119,34 +120,67 @@ export class RoomListStore2 extends AsyncStore<ActionPayload> {
         await this.updateAlgorithmInstances();
     }
 
-    private onRVSUpdate = () => {
+    /**
+     * Handles suspected RoomViewStore changes.
+     * @param trigger Set to false to prevent a list update from being sent. Should only
+     * be used if the calling code will manually trigger the update.
+     */
+    private async handleRVSUpdate({trigger = true}) {
         if (!this.enabled) return; // TODO: Remove with https://github.com/vector-im/riot-web/issues/14367
         if (!this.matrixClient) return; // We assume there won't be RVS updates without a client
 
         const activeRoomId = RoomViewStore.getRoomId();
         if (!activeRoomId && this.algorithm.stickyRoom) {
-            this.algorithm.stickyRoom = null;
+            await this.algorithm.setStickyRoom(null);
         } else if (activeRoomId) {
             const activeRoom = this.matrixClient.getRoom(activeRoomId);
             if (!activeRoom) {
                 console.warn(`${activeRoomId} is current in RVS but missing from client - clearing sticky room`);
-                this.algorithm.stickyRoom = null;
+                await this.algorithm.setStickyRoom(null);
             } else if (activeRoom !== this.algorithm.stickyRoom) {
-                // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
-                console.log(`Changing sticky room to ${activeRoomId}`);
-                this.algorithm.stickyRoom = activeRoom;
+                if (!window.mx_QuietRoomListLogging) {
+                    // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
+                    console.log(`Changing sticky room to ${activeRoomId}`);
+                }
+                await this.algorithm.setStickyRoom(activeRoom);
             }
         }
-    };
 
-    protected async onDispatch(payload: ActionPayload) {
+        if (trigger) this.updateFn.trigger();
+    }
+
+    protected onDispatch(payload: ActionPayload) {
+        // We do this to intentionally break out of the current event loop task, allowing
+        // us to instead wait for a more convenient time to run our updates.
+        setImmediate(() => this.onDispatchAsync(payload));
+    }
+
+    protected async onDispatchAsync(payload: ActionPayload) {
         if (payload.action === 'MatrixActions.sync') {
             // Filter out anything that isn't the first PREPARED sync.
             if (!(payload.prevState === 'PREPARED' && payload.state !== 'PREPARED')) {
                 return;
             }
 
+<<<<<<<
             await this.makeReady(payload.matrixClient);
+=======
+            // TODO: Remove with https://github.com/vector-im/riot-web/issues/14231
+            this.checkEnabled();
+            if (!this.enabled) return;
+
+            this._matrixClient = payload.matrixClient;
+
+            // Update any settings here, as some may have happened before we were logically ready.
+            console.log("Regenerating room lists: Startup");
+            await this.readAndCacheSettingsFromStore();
+            await this.regenerateAllLists({trigger: false});
+            await this.handleRVSUpdate({trigger: false}); // fake an RVS update to adjust sticky room, if needed
+
+            this.updateFn.trigger();
+
+            return; // no point in running the next conditions - they won't match
+>>>>>>>
         }
 
         // TODO: Remove this once the RoomListStore becomes default
@@ -155,7 +189,7 @@ export class RoomListStore2 extends AsyncStore<ActionPayload> {
         if (payload.action === 'on_client_not_viable' || payload.action === 'on_logged_out') {
             // Reset state without causing updates as the client will have been destroyed
             // and downstream code will throw NPE errors.
-            this.reset(null, true);
+            await this.reset(null, true);
             this._matrixClient = null;
             this.initialListsGenerated = false; // we'll want to regenerate them
         }
@@ -169,7 +203,8 @@ export class RoomListStore2 extends AsyncStore<ActionPayload> {
                 console.log("Regenerating room lists: Settings changed");
                 await this.readAndCacheSettingsFromStore();
 
-                await this.regenerateAllLists(); // regenerate the lists now
+                await this.regenerateAllLists({trigger: false}); // regenerate the lists now
+                this.updateFn.trigger();
             }
         }
 
@@ -187,16 +222,22 @@ export class RoomListStore2 extends AsyncStore<ActionPayload> {
                     console.warn(`Own read receipt was in unknown room ${room.roomId}`);
                     return;
                 }
-                // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
-                console.log(`[RoomListDebug] Got own read receipt in ${room.roomId}`);
+                if (!window.mx_QuietRoomListLogging) {
+                    // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
+                    console.log(`[RoomListDebug] Got own read receipt in ${room.roomId}`);
+                }
                 await this.handleRoomUpdate(room, RoomUpdateCause.ReadReceipt);
+                this.updateFn.trigger();
                 return;
             }
         } else if (payload.action === 'MatrixActions.Room.tags') {
             const roomPayload = (<any>payload); // TODO: Type out the dispatcher types
-            // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
-            console.log(`[RoomListDebug] Got tag change in ${roomPayload.room.roomId}`);
+            if (!window.mx_QuietRoomListLogging) {
+                // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
+                console.log(`[RoomListDebug] Got tag change in ${roomPayload.room.roomId}`);
+            }
             await this.handleRoomUpdate(roomPayload.room, RoomUpdateCause.PossibleTagChange);
+            this.updateFn.trigger();
         } else if (payload.action === 'MatrixActions.Room.timeline') {
             const eventPayload = (<any>payload); // TODO: Type out the dispatcher types
 
@@ -206,12 +247,16 @@ export class RoomListStore2 extends AsyncStore<ActionPayload> {
             const roomId = eventPayload.event.getRoomId();
             const room = this.matrixClient.getRoom(roomId);
             const tryUpdate = async (updatedRoom: Room) => {
-                // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
-                console.log(`[RoomListDebug] Live timeline event ${eventPayload.event.getId()}` +
-                    ` in ${updatedRoom.roomId}`);
-                if (eventPayload.event.getType() === 'm.room.tombstone' && eventPayload.event.getStateKey() === '') {
+                if (!window.mx_QuietRoomListLogging) {
                     // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
-                    console.log(`[RoomListDebug] Got tombstone event - trying to remove now-dead room`);
+                    console.log(`[RoomListDebug] Live timeline event ${eventPayload.event.getId()}` +
+                        ` in ${updatedRoom.roomId}`);
+                }
+                if (eventPayload.event.getType() === 'm.room.tombstone' && eventPayload.event.getStateKey() === '') {
+                    if (!window.mx_QuietRoomListLogging) {
+                        // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
+                        console.log(`[RoomListDebug] Got tombstone event - trying to remove now-dead room`);
+                    }
                     const newRoom = this.matrixClient.getRoom(eventPayload.event.getContent()['replacement_room']);
                     if (newRoom) {
                         // If we have the new room, then the new room check will have seen the predecessor
@@ -220,6 +265,7 @@ export class RoomListStore2 extends AsyncStore<ActionPayload> {
                     }
                 }
                 await this.handleRoomUpdate(updatedRoom, RoomUpdateCause.Timeline);
+                this.updateFn.trigger();
             };
             if (!room) {
                 console.warn(`Live timeline event ${eventPayload.event.getId()} received without associated room`);
@@ -240,13 +286,18 @@ export class RoomListStore2 extends AsyncStore<ActionPayload> {
                 console.warn(`Event ${eventPayload.event.getId()} was decrypted in an unknown room ${roomId}`);
                 return;
             }
-            // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
-            console.log(`[RoomListDebug] Decrypted timeline event ${eventPayload.event.getId()} in ${roomId}`);
+            if (!window.mx_QuietRoomListLogging) {
+                // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
+                console.log(`[RoomListDebug] Decrypted timeline event ${eventPayload.event.getId()} in ${roomId}`);
+            }
             await this.handleRoomUpdate(room, RoomUpdateCause.Timeline);
+            this.updateFn.trigger();
         } else if (payload.action === 'MatrixActions.accountData' && payload.event_type === 'm.direct') {
             const eventPayload = (<any>payload); // TODO: Type out the dispatcher types
-            // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
-            console.log(`[RoomListDebug] Received updated DM map`);
+            if (!window.mx_QuietRoomListLogging) {
+                // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
+                console.log(`[RoomListDebug] Received updated DM map`);
+            }
             const dmMap = eventPayload.event.getContent();
             for (const userId of Object.keys(dmMap)) {
                 const roomIds = dmMap[userId];
@@ -264,51 +315,73 @@ export class RoomListStore2 extends AsyncStore<ActionPayload> {
                     await this.handleRoomUpdate(room, RoomUpdateCause.PossibleTagChange);
                 }
             }
+            this.updateFn.trigger();
         } else if (payload.action === 'MatrixActions.Room.myMembership') {
             const membershipPayload = (<any>payload); // TODO: Type out the dispatcher types
             const oldMembership = getEffectiveMembership(membershipPayload.oldMembership);
             const newMembership = getEffectiveMembership(membershipPayload.membership);
             if (oldMembership !== EffectiveMembership.Join && newMembership === EffectiveMembership.Join) {
-                // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
-                console.log(`[RoomListDebug] Handling new room ${membershipPayload.room.roomId}`);
+                if (!window.mx_QuietRoomListLogging) {
+                    // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
+                    console.log(`[RoomListDebug] Handling new room ${membershipPayload.room.roomId}`);
+                }
 
                 // If we're joining an upgraded room, we'll want to make sure we don't proliferate
                 // the dead room in the list.
                 const createEvent = membershipPayload.room.currentState.getStateEvents("m.room.create", "");
                 if (createEvent && createEvent.getContent()['predecessor']) {
-                    console.log(`[RoomListDebug] Room has a predecessor`);
+                    if (!window.mx_QuietRoomListLogging) {
+                        // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
+                        console.log(`[RoomListDebug] Room has a predecessor`);
+                    }
                     const prevRoom = this.matrixClient.getRoom(createEvent.getContent()['predecessor']['room_id']);
                     if (prevRoom) {
                         const isSticky = this.algorithm.stickyRoom === prevRoom;
                         if (isSticky) {
-                            console.log(`[RoomListDebug] Clearing sticky room due to room upgrade`);
-                            await this.algorithm.setStickyRoomAsync(null);
+                            if (!window.mx_QuietRoomListLogging) {
+                                // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
+                                console.log(`[RoomListDebug] Clearing sticky room due to room upgrade`);
+                            }
+                            await this.algorithm.setStickyRoom(null);
                         }
 
                         // Note: we hit the algorithm instead of our handleRoomUpdate() function to
                         // avoid redundant updates.
-                        console.log(`[RoomListDebug] Removing previous room from room list`);
+                        if (!window.mx_QuietRoomListLogging) {
+                            // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
+                            console.log(`[RoomListDebug] Removing previous room from room list`);
+                        }
                         await this.algorithm.handleRoomUpdate(prevRoom, RoomUpdateCause.RoomRemoved);
                     }
                 }
 
-                console.log(`[RoomListDebug] Adding new room to room list`);
+                if (!window.mx_QuietRoomListLogging) {
+                    // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
+                    console.log(`[RoomListDebug] Adding new room to room list`);
+                }
                 await this.handleRoomUpdate(membershipPayload.room, RoomUpdateCause.NewRoom);
+                this.updateFn.trigger();
                 return;
             }
 
             if (oldMembership !== EffectiveMembership.Invite && newMembership === EffectiveMembership.Invite) {
-                // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
-                console.log(`[RoomListDebug] Handling invite to ${membershipPayload.room.roomId}`);
+                if (!window.mx_QuietRoomListLogging) {
+                    // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
+                    console.log(`[RoomListDebug] Handling invite to ${membershipPayload.room.roomId}`);
+                }
                 await this.handleRoomUpdate(membershipPayload.room, RoomUpdateCause.NewRoom);
+                this.updateFn.trigger();
                 return;
             }
 
             // If it's not a join, it's transitioning into a different list (possibly historical)
             if (oldMembership !== newMembership) {
-                // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
-                console.log(`[RoomListDebug] Handling membership change in ${membershipPayload.room.roomId}`);
+                if (!window.mx_QuietRoomListLogging) {
+                    // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
+                    console.log(`[RoomListDebug] Handling membership change in ${membershipPayload.room.roomId}`);
+                }
                 await this.handleRoomUpdate(membershipPayload.room, RoomUpdateCause.PossibleTagChange);
+                this.updateFn.trigger();
                 return;
             }
         }
@@ -317,9 +390,11 @@ export class RoomListStore2 extends AsyncStore<ActionPayload> {
     private async handleRoomUpdate(room: Room, cause: RoomUpdateCause): Promise<any> {
         const shouldUpdate = await this.algorithm.handleRoomUpdate(room, cause);
         if (shouldUpdate) {
-            // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
-            console.log(`[DEBUG] Room "${room.name}" (${room.roomId}) triggered by ${cause} requires list update`);
-            this.emit(LISTS_UPDATE_EVENT, this);
+            if (!window.mx_QuietRoomListLogging) {
+                // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
+                console.log(`[DEBUG] Room "${room.name}" (${room.roomId}) triggered by ${cause} requires list update`);
+            }
+            this.updateFn.mark();
         }
     }
 
@@ -327,6 +402,7 @@ export class RoomListStore2 extends AsyncStore<ActionPayload> {
         await this.algorithm.setTagSorting(tagId, sort);
         // TODO: Per-account? https://github.com/vector-im/riot-web/issues/14114
         localStorage.setItem(`mx_tagSort_${tagId}`, sort);
+        this.updateFn.triggerIfWillMark();
     }
 
     public getTagSorting(tagId: TagID): SortAlgorithm {
@@ -365,6 +441,7 @@ export class RoomListStore2 extends AsyncStore<ActionPayload> {
         await this.algorithm.setListOrdering(tagId, order);
         // TODO: Per-account? https://github.com/vector-im/riot-web/issues/14114
         localStorage.setItem(`mx_listOrder_${tagId}`, order);
+        this.updateFn.triggerIfWillMark();
     }
 
     public getListOrder(tagId: TagID): ListAlgorithm {
@@ -400,6 +477,10 @@ export class RoomListStore2 extends AsyncStore<ActionPayload> {
     }
 
     private async updateAlgorithmInstances() {
+        // We'll require an update, so mark for one. Marking now also prevents the calls
+        // to setTagSorting and setListOrder from causing triggers.
+        this.updateFn.mark();
+
         for (const tag of Object.keys(this.orderedLists)) {
             const definedSort = this.getTagSorting(tag);
             const definedOrder = this.getListOrder(tag);
@@ -423,13 +504,24 @@ export class RoomListStore2 extends AsyncStore<ActionPayload> {
     }
 
     private onAlgorithmListUpdated = () => {
-        // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
-        console.log("Underlying algorithm has triggered a list update - refiring");
-        this.emit(LISTS_UPDATE_EVENT, this);
+        if (!window.mx_QuietRoomListLogging) {
+            // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
+            console.log("Underlying algorithm has triggered a list update - marking");
+        }
+        this.updateFn.mark();
     };
 
+<<<<<<<
     // This is only exposed externally for the tests. Do not call this within the app.
     public async regenerateAllLists() {
+=======
+    /**
+     * Regenerates the room whole room list, discarding any previous results.
+     * @param trigger Set to false to prevent a list update from being sent. Should only
+     * be used if the calling code will manually trigger the update.
+     */
+    private async regenerateAllLists({trigger = true}) {
+>>>>>>>
         console.warn("Regenerating all room lists");
 
         const sorts: ITagSortingMap = {};
@@ -454,21 +546,26 @@ export class RoomListStore2 extends AsyncStore<ActionPayload> {
 
         this.initialListsGenerated = true;
 
-        this.emit(LISTS_UPDATE_EVENT, this);
+        if (trigger) this.updateFn.trigger();
     }
 
     public addFilter(filter: IFilterCondition): void {
-        // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
-        console.log("Adding filter condition:", filter);
+        if (!window.mx_QuietRoomListLogging) {
+            // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
+            console.log("Adding filter condition:", filter);
+        }
         this.filterConditions.push(filter);
         if (this.algorithm) {
             this.algorithm.addFilterCondition(filter);
         }
+        this.updateFn.trigger();
     }
 
     public removeFilter(filter: IFilterCondition): void {
-        // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
-        console.log("Removing filter condition:", filter);
+        if (!window.mx_QuietRoomListLogging) {
+            // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
+            console.log("Removing filter condition:", filter);
+        }
         const idx = this.filterConditions.indexOf(filter);
         if (idx >= 0) {
             this.filterConditions.splice(idx, 1);
@@ -477,6 +574,7 @@ export class RoomListStore2 extends AsyncStore<ActionPayload> {
                 this.algorithm.removeFilterCondition(filter);
             }
         }
+        this.updateFn.trigger();
     }
 
     /**
diff --git a/src/stores/room-list/algorithms/Algorithm.ts b/src/stores/room-list/algorithms/Algorithm.ts
index 35511a461d..d985abd392 100644
--- a/src/stores/room-list/algorithms/Algorithm.ts
+++ b/src/stores/room-list/algorithms/Algorithm.ts
@@ -87,12 +87,6 @@ export class Algorithm extends EventEmitter {
         return this._stickyRoom ? this._stickyRoom.room : null;
     }
 
-    public set stickyRoom(val: Room) {
-        // setters can't be async, so we call a private function to do the work
-        // noinspection JSIgnoredPromiseFromCall
-        this.updateStickyRoom(val);
-    }
-
     protected get hasFilters(): boolean {
         return this.allowedByFilter.size > 0;
     }
@@ -115,7 +109,7 @@ export class Algorithm extends EventEmitter {
      * Awaitable version of the sticky room setter.
      * @param val The new room to sticky.
      */
-    public async setStickyRoomAsync(val: Room) {
+    public async setStickyRoom(val: Room) {
         await this.updateStickyRoom(val);
     }
 
@@ -321,8 +315,10 @@ export class Algorithm extends EventEmitter {
             }
             newMap[tagId] = allowedRoomsInThisTag;
 
-            // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
-            console.log(`[DEBUG] ${newMap[tagId].length}/${rooms.length} rooms filtered into ${tagId}`);
+            if (!window.mx_QuietRoomListLogging) {
+                // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
+                console.log(`[DEBUG] ${newMap[tagId].length}/${rooms.length} rooms filtered into ${tagId}`);
+            }
         }
 
         const allowedRooms = Object.values(newMap).reduce((rv, v) => { rv.push(...v); return rv; }, <Room[]>[]);
@@ -331,26 +327,13 @@ export class Algorithm extends EventEmitter {
         this.emit(LIST_UPDATED_EVENT);
     }
 
-    // TODO: Remove or use.
-    protected addPossiblyFilteredRoomsToTag(tagId: TagID, added: Room[]): void {
-        const filters = this.allowedByFilter.keys();
-        for (const room of added) {
-            for (const filter of filters) {
-                if (filter.isVisible(room)) {
-                    this.allowedRoomsByFilters.add(room);
-                    break;
-                }
-            }
-        }
-
-        // Now that we've updated the allowed rooms, recalculate the tag
-        this.recalculateFilteredRoomsForTag(tagId);
-    }
-
     protected recalculateFilteredRoomsForTag(tagId: TagID): void {
         if (!this.hasFilters) return; // don't bother doing work if there's nothing to do
 
-        console.log(`Recalculating filtered rooms for ${tagId}`);
+        if (!window.mx_QuietRoomListLogging) {
+            // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
+            console.log(`Recalculating filtered rooms for ${tagId}`);
+        }
         delete this.filteredRooms[tagId];
         const rooms = this.cachedRooms[tagId].map(r => r); // cheap clone
         this.tryInsertStickyRoomToFilterSet(rooms, tagId);
@@ -359,8 +342,10 @@ export class Algorithm extends EventEmitter {
             this.filteredRooms[tagId] = filteredRooms;
         }
 
-        // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
-        console.log(`[DEBUG] ${filteredRooms.length}/${rooms.length} rooms filtered into ${tagId}`);
+        if (!window.mx_QuietRoomListLogging) {
+            // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
+            console.log(`[DEBUG] ${filteredRooms.length}/${rooms.length} rooms filtered into ${tagId}`);
+        }
     }
 
     protected tryInsertStickyRoomToFilterSet(rooms: Room[], tagId: TagID) {
@@ -399,8 +384,10 @@ export class Algorithm extends EventEmitter {
         }
 
         if (!this._cachedStickyRooms || !updatedTag) {
-            // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
-            console.log(`Generating clone of cached rooms for sticky room handling`);
+            if (!window.mx_QuietRoomListLogging) {
+                // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
+                console.log(`Generating clone of cached rooms for sticky room handling`);
+            }
             const stickiedTagMap: ITagMap = {};
             for (const tagId of Object.keys(this.cachedRooms)) {
                 stickiedTagMap[tagId] = this.cachedRooms[tagId].map(r => r); // shallow clone
@@ -411,8 +398,10 @@ export class Algorithm extends EventEmitter {
         if (updatedTag) {
             // Update the tag indicated by the caller, if possible. This is mostly to ensure
             // our cache is up to date.
-            // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
-            console.log(`Replacing cached sticky rooms for ${updatedTag}`);
+            if (!window.mx_QuietRoomListLogging) {
+                // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
+                console.log(`Replacing cached sticky rooms for ${updatedTag}`);
+            }
             this._cachedStickyRooms[updatedTag] = this.cachedRooms[updatedTag].map(r => r); // shallow clone
         }
 
@@ -421,8 +410,10 @@ export class Algorithm extends EventEmitter {
         // we might have updated from the cache is also our sticky room.
         const sticky = this._stickyRoom;
         if (!updatedTag || updatedTag === sticky.tag) {
-            // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
-            console.log(`Inserting sticky room ${sticky.room.roomId} at position ${sticky.position} in ${sticky.tag}`);
+            if (!window.mx_QuietRoomListLogging) {
+                // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
+                console.log(`Inserting sticky room ${sticky.room.roomId} at position ${sticky.position} in ${sticky.tag}`);
+            }
             this._cachedStickyRooms[sticky.tag].splice(sticky.position, 0, sticky.room);
         }
 
@@ -647,8 +638,10 @@ export class Algorithm extends EventEmitter {
      * processing.
      */
     public async handleRoomUpdate(room: Room, cause: RoomUpdateCause): Promise<boolean> {
-        // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
-        console.log(`Handle room update for ${room.roomId} called with cause ${cause}`);
+        if (!window.mx_QuietRoomListLogging) {
+            // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
+            console.log(`Handle room update for ${room.roomId} called with cause ${cause}`);
+        }
         if (!this.algorithms) throw new Error("Not ready: no algorithms to determine tags from");
 
         // Note: check the isSticky against the room ID just in case the reference is wrong
@@ -705,16 +698,20 @@ export class Algorithm extends EventEmitter {
             const diff = arrayDiff(oldTags, newTags);
             if (diff.removed.length > 0 || diff.added.length > 0) {
                 for (const rmTag of diff.removed) {
-                    // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
-                    console.log(`Removing ${room.roomId} from ${rmTag}`);
+                    if (!window.mx_QuietRoomListLogging) {
+                        // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
+                        console.log(`Removing ${room.roomId} from ${rmTag}`);
+                    }
                     const algorithm: OrderingAlgorithm = this.algorithms[rmTag];
                     if (!algorithm) throw new Error(`No algorithm for ${rmTag}`);
                     await algorithm.handleRoomUpdate(room, RoomUpdateCause.RoomRemoved);
                     this.cachedRooms[rmTag] = algorithm.orderedRooms;
                 }
                 for (const addTag of diff.added) {
-                    // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
-                    console.log(`Adding ${room.roomId} to ${addTag}`);
+                    if (!window.mx_QuietRoomListLogging) {
+                        // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
+                        console.log(`Adding ${room.roomId} to ${addTag}`);
+                    }
                     const algorithm: OrderingAlgorithm = this.algorithms[addTag];
                     if (!algorithm) throw new Error(`No algorithm for ${addTag}`);
                     await algorithm.handleRoomUpdate(room, RoomUpdateCause.NewRoom);
@@ -724,13 +721,17 @@ export class Algorithm extends EventEmitter {
                 // Update the tag map so we don't regen it in a moment
                 this.roomIdsToTags[room.roomId] = newTags;
 
-                // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
-                console.log(`Changing update cause for ${room.roomId} to Timeline to sort rooms`);
+                if (!window.mx_QuietRoomListLogging) {
+                    // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
+                    console.log(`Changing update cause for ${room.roomId} to Timeline to sort rooms`);
+                }
                 cause = RoomUpdateCause.Timeline;
                 didTagChange = true;
             } else {
-                // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
-                console.warn(`Received no-op update for ${room.roomId} - changing to Timeline update`);
+                if (!window.mx_QuietRoomListLogging) {
+                    // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
+                    console.log(`Received no-op update for ${room.roomId} - changing to Timeline update`);
+                }
                 cause = RoomUpdateCause.Timeline;
             }
 
@@ -746,7 +747,7 @@ export class Algorithm extends EventEmitter {
                     };
                 } else {
                     // We have to clear the lock as the sticky room change will trigger updates.
-                    await this.setStickyRoomAsync(room);
+                    await this.setStickyRoom(room);
                 }
             }
         }
@@ -756,20 +757,27 @@ export class Algorithm extends EventEmitter {
         // as the sticky room relies on this.
         if (cause !== RoomUpdateCause.NewRoom && cause !== RoomUpdateCause.RoomRemoved) {
             if (this.stickyRoom === room) {
-                // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
-                console.warn(`[RoomListDebug] Received ${cause} update for sticky room ${room.roomId} - ignoring`);
+                if (!window.mx_QuietRoomListLogging) {
+                    // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
+                    console.warn(`[RoomListDebug] Received ${cause} update for sticky room ${room.roomId} - ignoring`);
+                }
                 return false;
             }
         }
 
         if (!this.roomIdsToTags[room.roomId]) {
             if (CAUSES_REQUIRING_ROOM.includes(cause)) {
-                console.warn(`Skipping tag update for ${room.roomId} because we don't know about the room`);
+                if (!window.mx_QuietRoomListLogging) {
+                    // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
+                    console.warn(`Skipping tag update for ${room.roomId} because we don't know about the room`);
+                }
                 return false;
             }
 
-            // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
-            console.log(`[RoomListDebug] Updating tags for room ${room.roomId} (${room.name})`);
+            if (!window.mx_QuietRoomListLogging) {
+                // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
+                console.log(`[RoomListDebug] Updating tags for room ${room.roomId} (${room.name})`);
+            }
 
             // Get the tags for the room and populate the cache
             const roomTags = this.getTagsForRoom(room).filter(t => !isNullOrUndefined(this.cachedRooms[t]));
@@ -780,12 +788,16 @@ export class Algorithm extends EventEmitter {
 
             this.roomIdsToTags[room.roomId] = roomTags;
 
-            // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
-            console.log(`[RoomListDebug] Updated tags for ${room.roomId}:`, roomTags);
+            if (!window.mx_QuietRoomListLogging) {
+                // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
+                console.log(`[RoomListDebug] Updated tags for ${room.roomId}:`, roomTags);
+            }
         }
 
-        // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
-        console.log(`[RoomListDebug] Reached algorithmic handling for ${room.roomId} and cause ${cause}`);
+        if (!window.mx_QuietRoomListLogging) {
+            // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
+            console.log(`[RoomListDebug] Reached algorithmic handling for ${room.roomId} and cause ${cause}`);
+        }
 
         const tags = this.roomIdsToTags[room.roomId];
         if (!tags) {
@@ -807,8 +819,10 @@ export class Algorithm extends EventEmitter {
             changed = true;
         }
 
-        // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
-        console.log(`[RoomListDebug] Finished handling ${room.roomId} with cause ${cause} (changed=${changed})`);
+        if (!window.mx_QuietRoomListLogging) {
+            // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
+            console.log(`[RoomListDebug] Finished handling ${room.roomId} with cause ${cause} (changed=${changed})`);
+        }
         return changed;
     }
 }
diff --git a/src/stores/room-list/algorithms/list-ordering/ImportanceAlgorithm.ts b/src/stores/room-list/algorithms/list-ordering/ImportanceAlgorithm.ts
index 3acd9f924e..88789d3a50 100644
--- a/src/stores/room-list/algorithms/list-ordering/ImportanceAlgorithm.ts
+++ b/src/stores/room-list/algorithms/list-ordering/ImportanceAlgorithm.ts
@@ -87,9 +87,6 @@ export class ImportanceAlgorithm extends OrderingAlgorithm {
 
     public constructor(tagId: TagID, initialSortingAlgorithm: SortAlgorithm) {
         super(tagId, initialSortingAlgorithm);
-
-        // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
-        console.log(`[RoomListDebug] Constructed an ImportanceAlgorithm for ${tagId}`);
     }
 
     // noinspection JSMethodCanBeStatic
diff --git a/src/stores/room-list/algorithms/list-ordering/NaturalAlgorithm.ts b/src/stores/room-list/algorithms/list-ordering/NaturalAlgorithm.ts
index 849c8a2877..ae1a2c98f6 100644
--- a/src/stores/room-list/algorithms/list-ordering/NaturalAlgorithm.ts
+++ b/src/stores/room-list/algorithms/list-ordering/NaturalAlgorithm.ts
@@ -28,9 +28,6 @@ export class NaturalAlgorithm extends OrderingAlgorithm {
 
     public constructor(tagId: TagID, initialSortingAlgorithm: SortAlgorithm) {
         super(tagId, initialSortingAlgorithm);
-
-        // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
-        console.log(`[RoomListDebug] Constructed a NaturalAlgorithm for ${tagId}`);
     }
 
     public async setRooms(rooms: Room[]): Promise<any> {
diff --git a/src/stores/room-list/filters/CommunityFilterCondition.ts b/src/stores/room-list/filters/CommunityFilterCondition.ts
index 9f7d8daaa3..45e65fb4f4 100644
--- a/src/stores/room-list/filters/CommunityFilterCondition.ts
+++ b/src/stores/room-list/filters/CommunityFilterCondition.ts
@@ -52,8 +52,6 @@ export class CommunityFilterCondition extends EventEmitter implements IFilterCon
         const beforeRoomIds = this.roomIds;
         this.roomIds = (await GroupStore.getGroupRooms(this.community.groupId)).map(r => r.roomId);
         if (arrayHasDiff(beforeRoomIds, this.roomIds)) {
-            // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
-            console.log("Updating filter for group: ", this.community.groupId);
             this.emit(FILTER_CHANGED);
         }
     };
diff --git a/src/stores/room-list/filters/NameFilterCondition.ts b/src/stores/room-list/filters/NameFilterCondition.ts
index 12f147990d..6014a122f8 100644
--- a/src/stores/room-list/filters/NameFilterCondition.ts
+++ b/src/stores/room-list/filters/NameFilterCondition.ts
@@ -41,8 +41,6 @@ export class NameFilterCondition extends EventEmitter implements IFilterConditio
 
     public set search(val: string) {
         this._search = val;
-        // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
-        console.log("Updating filter for room name search:", this._search);
         this.emit(FILTER_CHANGED);
     }
 
diff --git a/src/utils/MarkedExecution.ts b/src/utils/MarkedExecution.ts
new file mode 100644
index 0000000000..de6cf05953
--- /dev/null
+++ b/src/utils/MarkedExecution.ts
@@ -0,0 +1,67 @@
+/*
+Copyright 2020 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+/**
+ * A utility to ensure that a function is only called once triggered with
+ * a mark applied. Multiple marks can be applied to the function, however
+ * the function will only be called once upon trigger().
+ *
+ * The function starts unmarked.
+ */
+export class MarkedExecution {
+    private marked = false;
+
+    /**
+     * Creates a MarkedExecution for the provided function.
+     * @param fn The function to be called upon trigger if marked.
+     */
+    constructor(private fn: () => void) {
+    }
+
+    /**
+     * Resets the mark without calling the function.
+     */
+    public reset() {
+        this.marked = false;
+    }
+
+    /**
+     * Marks the function to be called upon trigger().
+     */
+    public mark() {
+        this.marked = true;
+    }
+
+    /**
+     * If marked, the function will be called, otherwise this does nothing.
+     */
+    public trigger() {
+        if (!this.marked) return;
+        this.reset(); // reset first just in case the fn() causes a trigger()
+        this.fn();
+    }
+
+    /**
+     * Triggers the function if a mark() call would mark it. If the function
+     * has already been marked this will do nothing.
+     */
+    public triggerIfWillMark() {
+        if (!this.marked) {
+            this.mark();
+            this.trigger();
+        }
+    }
+}