diff --git a/src/display/operations.js b/src/display/operations.js
index e18fd084c9765feabd0519450f5cefa8478caba2..566eb5aa853b6991526c28550f3000456b9bb0e9 100644
--- a/src/display/operations.js
+++ b/src/display/operations.js
@@ -7,7 +7,7 @@ import { finishOperation, pushOperation } from "../util/operation_group"
 
 import { ensureFocus } from "./focus"
 import { measureForScrollbars, updateScrollbars } from "./scrollbars"
-import { setScrollLeft } from "./scroll_events"
+import { setScrollLeft, setScrollTop } from "./scroll_events"
 import { restartBlink } from "./selection"
 import { maybeScrollWindow, scrollPosIntoView } from "./scrolling"
 import { DisplayUpdate, maybeClipScrollbars, postUpdateDisplay, setDocumentHeight, updateDisplayIfNeeded } from "./update_display"
@@ -141,11 +141,8 @@ function endOperation_finish(op) {
     display.wheelStartX = display.wheelStartY = null
 
   // Propagate the scroll position to the actual DOM scroller
-  if (op.scrollTop != null && (display.scroller.scrollTop != op.scrollTop || op.forceScroll)) {
-    doc.scrollTop = Math.max(0, Math.min(display.scroller.scrollHeight - display.scroller.clientHeight, op.scrollTop))
-    display.scrollbars.setScrollTop(doc.scrollTop)
-    display.scroller.scrollTop = doc.scrollTop
-  }
+  if (op.scrollTop != null) setScrollTop(cm, op.scrollTop, true)
+
   if (op.scrollLeft != null) setScrollLeft(cm, op.scrollLeft, true, true)
   // If we need to scroll a specific position into view, do so.
   if (op.scrollToPos) {
diff --git a/src/display/scroll_events.js b/src/display/scroll_events.js
index 38d91441c0a28f8c079b33b5d934c22820073d0e..b7ce0f583d3422192ade04a93c759f1c04108a52 100644
--- a/src/display/scroll_events.js
+++ b/src/display/scroll_events.js
@@ -7,15 +7,21 @@ import { updateDisplaySimple} from "./update_display"
 
 // Sync the scrollable area and scrollbars, ensure the viewport
 // covers the visible area.
-export function setScrollTop(cm, val) {
+export function updateScrollTop(cm, val) {
   if (Math.abs(cm.doc.scrollTop - val) < 2) return
-  cm.doc.scrollTop = val
   if (!gecko) updateDisplaySimple(cm, {top: val})
-  if (cm.display.scroller.scrollTop != val) cm.display.scroller.scrollTop = val
-  cm.display.scrollbars.setScrollTop(val)
+  setScrollTop(cm, val)
   if (gecko) updateDisplaySimple(cm)
   startWorker(cm, 100)
 }
+export function setScrollTop(cm, val, forceScroll) {
+  val = Math.min(cm.display.scroller.scrollHeight - cm.display.scroller.clientHeight, val)
+  if (cm.display.scroller.scrollTop == val && !forceScroll) return
+  cm.doc.scrollTop = val
+  cm.display.scrollbars.setScrollTop(val)
+  cm.display.scroller.scrollTop = val
+}
+
 // Sync scroller and scrollbar, ensure the gutter elements are
 // aligned.
 export function setScrollLeft(cm, val, isScroller, forceScroll) {
@@ -94,7 +100,7 @@ export function onScrollWheel(cm, e) {
   // better than glitching out.
   if (dx && !gecko && !presto && wheelPixelsPerUnit != null) {
     if (dy && canScrollY)
-      setScrollTop(cm, Math.max(0, Math.min(scroll.scrollTop + dy * wheelPixelsPerUnit, scroll.scrollHeight - scroll.clientHeight)))
+      updateScrollTop(cm, Math.max(0, scroll.scrollTop + dy * wheelPixelsPerUnit))
     setScrollLeft(cm, Math.max(0, Math.min(scroll.scrollLeft + dx * wheelPixelsPerUnit, scroll.scrollWidth - scroll.clientWidth)))
     // Only prevent default scrolling if vertical scrolling is
     // actually possible. Otherwise, it causes vertical scroll
diff --git a/src/display/scrollbars.js b/src/display/scrollbars.js
index af31c78140180b11e45ff77472bba7ed9bd31461..d2f73c9154ebe10655075e78abab6483c595d880 100644
--- a/src/display/scrollbars.js
+++ b/src/display/scrollbars.js
@@ -5,7 +5,7 @@ import { ie, ie_version, mac, mac_geMountainLion } from "../util/browser"
 import { updateHeightsInViewport } from "./update_lines"
 import { Delayed } from "../util/misc"
 
-import { setScrollLeft, setScrollTop } from "./scroll_events"
+import { setScrollLeft, updateScrollTop } from "./scroll_events"
 
 // SCROLLBARS
 
@@ -185,7 +185,7 @@ export function initScrollbars(cm) {
     node.setAttribute("cm-not-content", "true")
   }, (pos, axis) => {
     if (axis == "horizontal") setScrollLeft(cm, pos)
-    else setScrollTop(cm, pos)
+    else updateScrollTop(cm, pos)
   }, cm)
   if (cm.display.scrollbars.addClass)
     addClass(cm.display.wrapper, cm.display.scrollbars.addClass)
diff --git a/src/display/scrolling.js b/src/display/scrolling.js
index b2deb2ed6444d56b5ac35c2fb4cae95b083fca51..5d06a63b500a375da0e739feddd2bf1fee472951 100644
--- a/src/display/scrolling.js
+++ b/src/display/scrolling.js
@@ -4,7 +4,7 @@ import { phantom } from "../util/browser"
 import { elt } from "../util/dom"
 import { signalDOMEvent } from "../util/event"
 
-import { setScrollLeft, setScrollTop } from "./scroll_events"
+import { setScrollLeft, updateScrollTop } from "./scroll_events"
 
 // SCROLLING THINGS INTO VIEW
 
@@ -44,7 +44,7 @@ export function scrollPosIntoView(cm, pos, end, margin) {
     let scrollPos = calculateScrollPos(cm, rect)
     let startTop = cm.doc.scrollTop, startLeft = cm.doc.scrollLeft
     if (scrollPos.scrollTop != null) {
-      setScrollTop(cm, scrollPos.scrollTop)
+      updateScrollTop(cm, scrollPos.scrollTop)
       if (Math.abs(cm.doc.scrollTop - startTop) > 1) changed = true
     }
     if (scrollPos.scrollLeft != null) {
@@ -59,7 +59,7 @@ export function scrollPosIntoView(cm, pos, end, margin) {
 // Scroll a given set of coordinates into view (immediately).
 export function scrollIntoView(cm, rect) {
   let scrollPos = calculateScrollPos(cm, rect)
-  if (scrollPos.scrollTop != null) setScrollTop(cm, scrollPos.scrollTop)
+  if (scrollPos.scrollTop != null) updateScrollTop(cm, scrollPos.scrollTop)
   if (scrollPos.scrollLeft != null) setScrollLeft(cm, scrollPos.scrollLeft)
 }
 
diff --git a/src/edit/CodeMirror.js b/src/edit/CodeMirror.js
index 2d502c9ebc6f5a0c5a4f3ef8ebc56ddbb98788ca..5f66c9f4f715a6b3ca0b9312889fc117d554bb52 100644
--- a/src/edit/CodeMirror.js
+++ b/src/edit/CodeMirror.js
@@ -4,7 +4,7 @@ import { setGuttersForLineNumbers, updateGutters } from "../display/gutters"
 import { maybeUpdateLineNumberWidth } from "../display/line_numbers"
 import { endOperation, operation, startOperation } from "../display/operations"
 import { initScrollbars } from "../display/scrollbars"
-import { onScrollWheel, setScrollLeft, setScrollTop } from "../display/scroll_events"
+import { onScrollWheel, setScrollLeft, updateScrollTop } from "../display/scroll_events"
 import { clipPos, Pos } from "../line/pos"
 import { posFromMouse } from "../measurement/position_measurement"
 import { eventInWidget } from "../measurement/widgets"
@@ -180,7 +180,7 @@ function registerEventHandlers(cm) {
   // area, ensure viewport is updated when scrolling.
   on(d.scroller, "scroll", () => {
     if (d.scroller.clientHeight) {
-      setScrollTop(cm, d.scroller.scrollTop)
+      updateScrollTop(cm, d.scroller.scrollTop)
       setScrollLeft(cm, d.scroller.scrollLeft, true)
       signal(cm, "scroll", cm)
     }