diff --git a/CHANGES.md b/CHANGES.md
index efbab5bd27..bcd8b0dec0 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -6,6 +6,7 @@ Features ✨:
 
 Improvements 🙌:
  - PIN code: request PIN code if phone has been locked
+ - Small optimisation of scrolling experience in timeline (#2114)
 
 Bugfix 🐛:
  - Fix Splash layout on small screens
@@ -21,6 +22,7 @@ Build 🧱:
 
 Other changes:
  - Added registration/verification automated UI tests
+ - Create a script to help getting public information form any homeserver
 
 Changes in Element 1.0.8 (2020-09-25)
 ===================================================
diff --git a/tools/hs_diag.py b/tools/hs_diag.py
new file mode 100755
index 0000000000..ded10a562d
--- /dev/null
+++ b/tools/hs_diag.py
@@ -0,0 +1,69 @@
+#!/usr/bin/env python3
+
+#  Copyright (c) 2020 New Vector Ltd
+#
+#  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.
+
+import argparse
+import os
+
+### Arguments
+
+parser = argparse.ArgumentParser(description='Get some information about a homeserver.')
+parser.add_argument('-s',
+                    '--homeserver',
+                    required=True,
+                    help="homeserver URL")
+parser.add_argument('-v',
+                    '--verbose',
+                    help="increase output verbosity.",
+                    action="store_true")
+
+args = parser.parse_args()
+
+if args.verbose:
+    print("Argument:")
+    print(args)
+
+baseUrl = args.homeserver
+
+if not baseUrl.startswith("http"):
+    baseUrl = "https://" + baseUrl
+
+if not baseUrl.endswith("/"):
+    baseUrl = baseUrl + "/"
+
+print("Get information from " + baseUrl)
+
+items = [
+    # [Title, URL, True for GET request and False for POST request]
+    ["Well-known", baseUrl + ".well-known/matrix/client", True]
+    , ["Version", baseUrl + "_matrix/client/versions", True]
+    , ["Login flow", baseUrl + "_matrix/client/r0/login", True]
+    , ["Registration flow", baseUrl + "_matrix/client/r0/register", False]
+    # Useless , ["Username availability", baseUrl + "_matrix/client/r0/register/available?username=benoit", True]
+    # Useless , ["Public rooms", baseUrl + "_matrix/client/r0/publicRooms?limit=1", True]
+    # Useless , ["Profile", baseUrl + "_matrix/client/r0/profile/@benoit.marty:matrix.org", True]
+    # Need token , ["Capability", baseUrl + "_matrix/client/r0/capabilities", True]
+    # Need token , ["Media config", baseUrl + "_matrix/media/r0/config", True]
+    # Need token , ["Turn", baseUrl + "_matrix/client/r0/voip/turnServer", True]
+]
+
+for item in items:
+    print("====================================================================================================")
+    print("# " + item[0] + " (" + item[1] + ")")
+    print("====================================================================================================")
+    if item[2]:
+        os.system("curl -s -X GET '" + item[1] + "' | python -m json.tool")
+    else:
+        os.system("curl -s -X POST --data $'{}' '" + item[1] + "' | python -m json.tool")
diff --git a/vector/build.gradle b/vector/build.gradle
index 9191ce640c..222c0c5cb3 100644
--- a/vector/build.gradle
+++ b/vector/build.gradle
@@ -344,6 +344,7 @@ dependencies {
     implementation 'com.jakewharton.rxbinding3:rxbinding-material:3.0.0'
 
     implementation("com.airbnb.android:epoxy:$epoxy_version")
+    implementation "com.airbnb.android:epoxy-glide-preloading:$epoxy_version"
     kapt "com.airbnb.android:epoxy-processor:$epoxy_version"
     implementation "com.airbnb.android:epoxy-paging:$epoxy_version"
     implementation 'com.airbnb.android:mvrx:1.3.0'
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt
index 7c3ac6011e..e7140f06f4 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt
@@ -53,6 +53,8 @@ import androidx.recyclerview.widget.RecyclerView
 import butterknife.BindView
 import com.airbnb.epoxy.EpoxyModel
 import com.airbnb.epoxy.OnModelBuildFinishedListener
+import com.airbnb.epoxy.addGlidePreloader
+import com.airbnb.epoxy.glidePreloader
 import com.airbnb.mvrx.Async
 import com.airbnb.mvrx.Fail
 import com.airbnb.mvrx.Loading
@@ -75,6 +77,7 @@ import im.vector.app.core.extensions.setTextOrHide
 import im.vector.app.core.extensions.showKeyboard
 import im.vector.app.core.extensions.trackItemsVisibilityChange
 import im.vector.app.core.glide.GlideApp
+import im.vector.app.core.glide.GlideRequests
 import im.vector.app.core.intent.getMimeTypeFromUri
 import im.vector.app.core.platform.VectorBaseFragment
 import im.vector.app.core.resources.ColorProvider
@@ -218,7 +221,8 @@ class RoomDetailFragment @Inject constructor(
         private val colorProvider: ColorProvider,
         private val notificationUtils: NotificationUtils,
         private val webRtcPeerConnectionManager: WebRtcPeerConnectionManager,
-        private val matrixItemColorProvider: MatrixItemColorProvider
+        private val matrixItemColorProvider: MatrixItemColorProvider,
+        private val imageContentRenderer: ImageContentRenderer
 ) :
         VectorBaseFragment(),
         TimelineEventController.Callback,
@@ -921,6 +925,16 @@ class RoomDetailFragment @Inject constructor(
             val touchHelper = ItemTouchHelper(swipeCallback)
             touchHelper.attachToRecyclerView(recyclerView)
         }
+        recyclerView.addGlidePreloader(
+                epoxyController = timelineEventController,
+                requestManager = GlideApp.with(this),
+                preloader = glidePreloader { requestManager, epoxyModel: MessageImageVideoItem, _ ->
+                    imageContentRenderer.createGlideRequest(
+                            epoxyModel.mediaData,
+                            ImageContentRenderer.Mode.THUMBNAIL,
+                            requestManager as GlideRequests
+                    )
+                })
     }
 
     private fun updateJumpToReadMarkerViewVisibility() {
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt
index be59128c26..56de0f7829 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt
@@ -56,6 +56,8 @@ import org.matrix.android.sdk.api.session.room.timeline.Timeline
 import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
 import javax.inject.Inject
 
+private const val DEFAULT_PREFETCH_THRESHOLD = 30
+
 class TimelineEventController @Inject constructor(private val dateFormatter: VectorDateFormatter,
                                                   private val contentUploadStateTrackerBinder: ContentUploadStateTrackerBinder,
                                                   private val contentDownloadStateTrackerBinder: ContentDownloadStateTrackerBinder,
@@ -116,6 +118,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
     private var unreadState: UnreadState = UnreadState.Unknown
     private var positionOfReadMarker: Int? = null
     private var eventIdToHighlight: String? = null
+    private var previousModelsSize = 0
 
     var callback: Callback? = null
     var timeline: Timeline? = null
@@ -191,6 +194,29 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
                 models.add(position, readMarker)
             }
         }
+        val shouldAddBackwardPrefetch = timeline?.hasMoreToLoad(Timeline.Direction.BACKWARDS) ?: false
+        if (shouldAddBackwardPrefetch) {
+            val indexOfPrefetchBackward = (previousModelsSize - 1)
+                    .coerceAtMost(models.size - DEFAULT_PREFETCH_THRESHOLD)
+                    .coerceAtLeast(0)
+
+            val loadingItem = LoadingItem_()
+                    .id("prefetch_backward_loading${System.currentTimeMillis()}")
+                    .showLoader(false)
+                    .setVisibilityStateChangedListener(Timeline.Direction.BACKWARDS)
+
+            models.add(indexOfPrefetchBackward, loadingItem)
+        }
+        val shouldAddForwardPrefetch = timeline?.hasMoreToLoad(Timeline.Direction.FORWARDS) ?: false
+        if (shouldAddForwardPrefetch) {
+            val indexOfPrefetchForward = DEFAULT_PREFETCH_THRESHOLD.coerceAtMost(models.size - 1)
+            val loadingItem = LoadingItem_()
+                    .id("prefetch_forward_loading${System.currentTimeMillis()}")
+                    .showLoader(false)
+                    .setVisibilityStateChangedListener(Timeline.Direction.FORWARDS)
+            models.add(indexOfPrefetchForward, loadingItem)
+        }
+        previousModelsSize = models.size
     }
 
     fun update(viewState: RoomDetailViewState) {
@@ -355,9 +381,6 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
         return shouldAdd
     }
 
-    /**
-     * Return true if added
-     */
     private fun LoadingItem_.setVisibilityStateChangedListener(direction: Timeline.Direction): LoadingItem_ {
         return onVisibilityStateChanged { _, _, visibilityState ->
             if (visibilityState == VisibilityState.VISIBLE) {
diff --git a/vector/src/main/java/im/vector/app/features/media/ImageContentRenderer.kt b/vector/src/main/java/im/vector/app/features/media/ImageContentRenderer.kt
index 0336e2d03c..d6ab24bfbd 100644
--- a/vector/src/main/java/im/vector/app/features/media/ImageContentRenderer.kt
+++ b/vector/src/main/java/im/vector/app/features/media/ImageContentRenderer.kt
@@ -33,6 +33,7 @@ import im.vector.app.R
 import im.vector.app.core.di.ActiveSessionHolder
 import im.vector.app.core.glide.GlideApp
 import im.vector.app.core.glide.GlideRequest
+import im.vector.app.core.glide.GlideRequests
 import im.vector.app.core.ui.model.Size
 import im.vector.app.core.utils.DimensionConverter
 import im.vector.app.core.utils.isLocalFile
@@ -206,12 +207,14 @@ class ImageContentRenderer @Inject constructor(private val activeSessionHolder:
                 .into(imageView)
     }
 
-    private fun createGlideRequest(data: Data, mode: Mode, imageView: ImageView, size: Size): GlideRequest<Drawable> {
+    fun createGlideRequest(data: Data, mode: Mode, imageView: ImageView, size: Size): GlideRequest<Drawable> {
+        return createGlideRequest(data, mode, GlideApp.with(imageView), size)
+    }
+
+    fun createGlideRequest(data: Data, mode: Mode, glideRequests: GlideRequests, size: Size = processSize(data, mode)): GlideRequest<Drawable> {
         return if (data.elementToDecrypt != null) {
             // Encrypted image
-            GlideApp
-                    .with(imageView)
-                    .load(data)
+            glideRequests.load(data)
         } else {
             // Clear image
             val contentUrlResolver = activeSessionHolder.getActiveSession().contentUrlResolver()
@@ -223,15 +226,12 @@ class ImageContentRenderer @Inject constructor(private val activeSessionHolder:
             // Fallback to base url
                     ?: data.url.takeIf { it?.startsWith("content://") == true }
 
-            GlideApp
-                    .with(imageView)
+           glideRequests
                     .load(resolvedUrl)
                     .apply {
                         if (mode == Mode.THUMBNAIL) {
                             error(
-                                    GlideApp
-                                            .with(imageView)
-                                            .load(resolveUrl(data))
+                                   glideRequests.load(resolveUrl(data))
                             )
                         }
                     }