diff --git a/vendor/magento/module-media-gallery-ui/Controller/Adminhtml/Directories/GetTree.php b/vendor/magento/module-media-gallery-ui/Controller/Adminhtml/Directories/GetTree.php
index 079b25cdc76..6084c244859 100644
--- a/vendor/magento/module-media-gallery-ui/Controller/Adminhtml/Directories/GetTree.php
+++ b/vendor/magento/module-media-gallery-ui/Controller/Adminhtml/Directories/GetTree.php
@@ -59,9 +59,19 @@ class GetTree extends Action implements HttpGetActionInterface
     public function execute()
     {
         try {
-            $responseContent =
-                $this->getDirectoryTree->execute()
-            ;
+            $path = $this->getRequest()->getParam('path');
+            $loadWholeTree = filter_var(
+                $this->getRequest()->getParam('loadWholeTree', true),
+                FILTER_VALIDATE_BOOLEAN
+            );
+            if ($path === '#' || $path === '') {
+                $path = null;
+            }
+
+            $responseContent = $this->getDirectoryTree->execute(
+                is_string($path) ? $path : null,
+                $loadWholeTree
+            );
             $responseCode = self::HTTP_OK;
         } catch (\Exception $exception) {
             $this->logger->critical($exception);
diff --git a/vendor/magento/module-media-gallery-ui/Model/Directories/GetDirectoryTree.php b/vendor/magento/module-media-gallery-ui/Model/Directories/GetDirectoryTree.php
index bff9d9867dd..592e3001f2d 100644
--- a/vendor/magento/module-media-gallery-ui/Model/Directories/GetDirectoryTree.php
+++ b/vendor/magento/module-media-gallery-ui/Model/Directories/GetDirectoryTree.php
@@ -12,7 +12,7 @@ use Magento\Framework\App\Filesystem\DirectoryList;
 use Magento\Framework\App\ObjectManager;
 use Magento\Framework\Exception\ValidatorException;
 use Magento\Framework\Filesystem;
-use Magento\Framework\Filesystem\Directory\Read;
+use Magento\Framework\Filesystem\Directory\ReadInterface;
 use Magento\MediaGalleryApi\Api\IsPathExcludedInterface;
 
 /**
@@ -53,27 +53,20 @@ class GetDirectoryTree
     }
 
     /**
-     * Return directory folder structure in array
+     * Return the directory folder structure in an array
      *
+     * @param string|null $path
+     * @param bool $loadWholeTree
      * @return array
      * @throws ValidatorException
      */
-    public function execute(): array
+    public function execute(?string $path = null, bool $loadWholeTree = true): array
     {
-        $tree = [
-            'name' => 'root',
-            'path' => '/',
-            'children' => []
-        ];
-        $directories = $this->getDirectories();
-        foreach ($directories as $idx => &$node) {
-            $node['children'] = [];
-            $result = $this->findParent($node, $tree);
-            $parent = &$result['treeNode'];
-
-            $parent['children'][] = &$directories[$idx];
+        if ($loadWholeTree) {
+            return $this->getDirectories();
         }
-        return $tree['children'];
+
+        return $this->getDirectoryNodesForPath($path);
     }
 
     /**
@@ -86,36 +79,109 @@ class GetDirectoryTree
     {
         $directories = [];
 
-        /** @var Read $mediaDirectory */
+        /** @var ReadInterface $mediaDirectory */
         $mediaDirectory = $this->filesystem->getDirectoryRead(DirectoryList::MEDIA);
 
         if ($mediaDirectory->isDirectory()) {
-            $imageFolderPaths = $this->coreConfig->getValue(
-                self::XML_PATH_MEDIA_GALLERY_IMAGE_FOLDERS,
-                ScopeConfigInterface::SCOPE_TYPE_DEFAULT
-            );
-            sort($imageFolderPaths);
-
-            foreach ($imageFolderPaths as $imageFolderPath) {
-                $imageDirectory = $this->filesystem->getDirectoryReadByPath(
-                    $mediaDirectory->getAbsolutePath($imageFolderPath)
-                );
-                if ($imageDirectory->isDirectory()) {
-                    $directories[] = $this->getDirectoryData($imageFolderPath);
-                    foreach ($imageDirectory->readRecursively() as $path) {
-                        if ($imageDirectory->isDirectory($path)) {
-                            $directories[] = $this->getDirectoryData(
-                                $mediaDirectory->getRelativePath($imageDirectory->getAbsolutePath($path))
-                            );
-                        }
-                    }
-                }
+            foreach ($this->getAllowedDirectoryPaths($mediaDirectory) as $imageFolderPath) {
+                $directories[] = $this->buildDirectoryNode($mediaDirectory, $imageFolderPath);
             }
         }
 
         return $directories;
     }
 
+    /**
+     * Return directory nodes for a specific path (or roots when path is empty).
+     *
+     * @param string|null $path
+     * @return array
+     * @throws ValidatorException
+     */
+    private function getDirectoryNodesForPath(?string $path): array
+    {
+        $nodes = [];
+
+        /** @var ReadInterface $mediaDirectory */
+        $mediaDirectory = $this->filesystem->getDirectoryRead(DirectoryList::MEDIA);
+        if (!$mediaDirectory->isDirectory()) {
+            return $nodes;
+        }
+
+        if ($path === null || $path === '') {
+            foreach ($this->getAllowedDirectoryPaths($mediaDirectory) as $rootPath) {
+                $nodes[] = $this->getDirectoryDataForLazyLoad($mediaDirectory, $rootPath);
+            }
+
+            return $nodes;
+        }
+
+        $normalizedPath = trim($path, '/');
+        if (!$this->isPathWithinAllowedRoots($normalizedPath, $mediaDirectory)) {
+            return $nodes;
+        }
+
+        if (!$mediaDirectory->isDirectory($normalizedPath)) {
+            return $nodes;
+        }
+
+        foreach ($this->getSubdirectoryPaths($mediaDirectory, $normalizedPath) as $subdirectoryPath) {
+            $nodes[] = $this->getDirectoryDataForLazyLoad($mediaDirectory, $subdirectoryPath);
+        }
+
+        return $nodes;
+    }
+    /**
+     * Check whether a path is inside the configured allowed roots.
+     *
+     * @param string $path
+     * @param ReadInterface $mediaDirectory
+     * @return bool
+     */
+    private function isPathWithinAllowedRoots(string $path, ReadInterface $mediaDirectory): bool
+    {
+        foreach ($this->getAllowedDirectoryPaths($mediaDirectory) as $allowedRoot) {
+            if ($path === $allowedRoot || strpos($path, $allowedRoot . '/') === 0) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    /**
+     * Return sorted and existing top-level media gallery paths.
+     *
+     * @param ReadInterface $mediaDirectory
+     * @return string[]
+     */
+    private function getAllowedDirectoryPaths(ReadInterface $mediaDirectory): array
+    {
+        $imageFolderPaths = $this->coreConfig->getValue(
+            self::XML_PATH_MEDIA_GALLERY_IMAGE_FOLDERS,
+            ScopeConfigInterface::SCOPE_TYPE_DEFAULT
+        );
+
+        if (!is_array($imageFolderPaths)) {
+            return [];
+        }
+
+        sort($imageFolderPaths);
+
+        $allowedDirectoryPaths = [];
+        foreach ($imageFolderPaths as $imageFolderPath) {
+            if (is_string($imageFolderPath)
+                && $imageFolderPath !== ''
+                && !$this->isPathExcluded->execute($imageFolderPath)
+                && $mediaDirectory->isDirectory($imageFolderPath)
+            ) {
+                $allowedDirectoryPaths[] = $imageFolderPath;
+            }
+        }
+
+        return $allowedDirectoryPaths;
+    }
+
     /**
      * Return jstree data for given path
      *
@@ -135,39 +201,78 @@ class GetDirectoryTree
     }
 
     /**
-     * Find parent directory
+     * Build directory tree recursively for a root folder.
      *
-     * @param array $node
-     * @param array $treeNode
-     * @param int $level
+     * @param ReadInterface $mediaDirectory
+     * @param string $path
      * @return array
      */
-    private function findParent(array &$node, array &$treeNode, int $level = 0): array
+    private function buildDirectoryNode(ReadInterface $mediaDirectory, string $path): array
     {
-        $nodePathLength = count($node['path_array']);
-        $treeNodeParentLevel = $nodePathLength - 1;
+        $node = $this->getDirectoryData($path);
+        $node['children'] = [];
 
-        $result = ['treeNode' => &$treeNode];
-
-        if ($nodePathLength <= 1 || $level > $treeNodeParentLevel) {
-            return $result;
+        foreach ($this->getSubdirectoryPaths($mediaDirectory, $path) as $subdirectoryPath) {
+            $node['children'][] = $this->buildDirectoryNode($mediaDirectory, $subdirectoryPath);
         }
 
-        foreach ($treeNode['children'] as &$tnode) {
-            $tNodePathLength = count($tnode['path_array']);
-            $found = false;
-            while ($level < $tNodePathLength) {
-                $found = $node['path_array'][$level] === $tnode['path_array'][$level];
-                if ($found) {
-                    $level ++;
-                } else {
-                    break;
-                }
+        return $node;
+    }
+
+    /**
+     * Build jstree node for on-demand loading.
+     *
+     * @param ReadInterface $mediaDirectory
+     * @param string $path
+     * @return array
+     */
+    private function getDirectoryDataForLazyLoad(ReadInterface $mediaDirectory, string $path): array
+    {
+        $node = $this->getDirectoryData($path);
+        $node['children'] = $this->hasSubdirectories($mediaDirectory, $path);
+
+        return $node;
+    }
+
+    /**
+     * Return sorted subdirectories for a given folder.
+     *
+     * @param ReadInterface $mediaDirectory
+     * @param string $path
+     * @return string[]
+     * @throws ValidatorException
+     */
+    private function getSubdirectoryPaths(ReadInterface $mediaDirectory, string $path): array
+    {
+        $subdirectories = [];
+
+        foreach ($mediaDirectory->read($path) as $entryPath) {
+            if ($mediaDirectory->isDirectory($entryPath) && !$this->isPathExcluded->execute($entryPath)) {
+                $subdirectories[] = $entryPath;
             }
-            if ($found) {
-                return $this->findParent($node, $tnode, $level);
+        }
+
+        sort($subdirectories);
+
+        return $subdirectories;
+    }
+
+    /**
+     * Check whether path has at least one visible subdirectory.
+     *
+     * @param ReadInterface $mediaDirectory
+     * @param string $path
+     * @return bool
+     * @throws ValidatorException
+     */
+    private function hasSubdirectories(ReadInterface $mediaDirectory, string $path): bool
+    {
+        foreach ($mediaDirectory->read($path) as $entryPath) {
+            if ($mediaDirectory->isDirectory($entryPath) && !$this->isPathExcluded->execute($entryPath)) {
+                return true;
             }
         }
-        return $result;
+
+        return false;
     }
 }
diff --git a/vendor/magento/module-media-gallery-ui/view/adminhtml/web/js/directory/directories.js b/vendor/magento/module-media-gallery-ui/view/adminhtml/web/js/directory/directories.js
index b3f62fc6673..0a63eb8ceab 100644
--- a/vendor/magento/module-media-gallery-ui/view/adminhtml/web/js/directory/directories.js
+++ b/vendor/magento/module-media-gallery-ui/view/adminhtml/web/js/directory/directories.js
@@ -74,7 +74,7 @@ define([
                             this.directoryTree().createDirectoryUrl,
                             [this.getNewFolderPath(folderName)]
                         ).then(function () {
-                            this.directoryTree().reloadJsTree().then(function () {
+                            this.directoryTree().reloadJsTree(true).then(function () {
                                 this.directoryTree().locateNode(this.getNewFolderPath(folderName));
                             }.bind(this));
                         }.bind(this)).fail(function (error) {
diff --git a/vendor/magento/module-media-gallery-ui/view/adminhtml/web/js/directory/directoryTree.js b/vendor/magento/module-media-gallery-ui/view/adminhtml/web/js/directory/directoryTree.js
index a4d2dff4abf..3f5f2f84c85 100644
--- a/vendor/magento/module-media-gallery-ui/view/adminhtml/web/js/directory/directoryTree.js
+++ b/vendor/magento/module-media-gallery-ui/view/adminhtml/web/js/directory/directoryTree.js
@@ -26,6 +26,7 @@ define([
             createDirectoryUrl: 'media_gallery/directories/create',
             deleteDirectoryUrl: 'media_gallery/directories/delete',
             jsTreeReloaded: null,
+            restoringDirectorySelection: false,
             modules: {
                 bookmarks: '${ $.bookmarkProvider }',
                 directories: '${ $.name }_directories',
@@ -67,10 +68,16 @@ define([
          * Render directory tree component.
          */
         renderDirectoryTree: function () {
-            return this.getJsonTree().then(function (data) {
+            if (this.isLazyTreeMode()) {
+                this.createTree();
+
+                return $.Deferred().resolve().promise();
+            }
+
+            return this.getJsonTree(true).then(function (data) {
                 this.createFolderIfNotExists(data).then(function (isFolderCreated) {
                     if (isFolderCreated) {
-                        this.getJsonTree().then(function (newData) {
+                        this.getJsonTree(true).then(function (newData) {
                             this.createTree(newData);
                         }.bind(this));
                     } else {
@@ -129,6 +136,15 @@ define([
             return deferred.promise();
         },
 
+        /**
+         * Check if directory tree should be loaded on demand.
+         *
+         * @returns {Boolean}
+         */
+        isLazyTreeMode: function () {
+            return _.isNull(this.getRequestedDirectory());
+        },
+
         /**
          * Verify if directory exists in array
          *
@@ -218,7 +234,7 @@ define([
             this.disableMultiselectBehavior();
 
             $(window).on('reload.MediaGallery', function () {
-                this.getJsonTree().then(function (data) {
+                this.getJsonTree(!this.isLazyTreeMode()).then(function (data) {
                     this.createFolderIfNotExists(data).then(function (isCreated) {
                         if (isCreated) {
                             this.renderDirectoryTree().then(function () {
@@ -251,10 +267,15 @@ define([
          * Verify directory filter on init event, select folder per directory filter state
          */
         updateSelectedDirectory: function () {
-            var currentFilterPath = this.filterChips().filters.path,
+            var appliedFilters = this.filterChips().get('applied') || {},
+                currentFilterPath = appliedFilters.path || this.filterChips().filters.path,
                 requestedDirectory = this.getRequestedDirectory(),
                 currentTreePath;
 
+            if (this.restoringDirectorySelection) {
+                return;
+            }
+
             if (_.isUndefined(currentFilterPath)) {
                 this.clearFiltersHandle();
 
@@ -275,9 +296,128 @@ define([
 
             if (this.folderExistsInTree(currentTreePath)) {
                 this.locateNode(currentTreePath);
-            } else {
-                this.selectStorageRoot();
+            } else if (_.isString(currentTreePath) && currentTreePath !== '') {
+                if (!this.isLazyTreeMode()) {
+                    this.selectStorageRoot();
+                    return;
+                }
+
+                this.restoringDirectorySelection = true;
+                this.ensurePathLoaded(currentTreePath)
+                    .done(function (isLoaded) {
+                        if (isLoaded && this.folderExistsInTree(currentTreePath)) {
+                            this.locateNode(currentTreePath);
+                        } else {
+                            this.selectStorageRoot();
+                        }
+                    }.bind(this))
+                    .always(function () {
+                        this.restoringDirectorySelection = false;
+                    }.bind(this));
+            }
+        },
+
+        /**
+         * Ensure all directory ancestors are loaded in lazy tree mode.
+         *
+         * @param {String} path
+         * @returns {jQuery.Promise}
+         */
+        ensurePathLoaded: function (path) {
+            var deferred = $.Deferred(),
+                pathChain = this.getPathChain(path),
+                index;
+
+            if (!_.isString(path) || path === '') {
+                deferred.resolve(false);
+                return deferred.promise();
+            }
+
+            if (!this.isLazyTreeMode()) {
+                deferred.resolve(this.folderExistsInTree(path));
+                return deferred.promise();
+            }
+
+            index = _.findIndex(pathChain, function (segmentPath) {
+                return !!this.folderExistsInTree(segmentPath);
+            }.bind(this));
+
+            if (index === -1) {
+                deferred.resolve(false);
+                return deferred.promise();
+            }
+
+            /**
+             * Open each path segment sequentially so jstree lazy-loads children.
+             */
+            function processNextSegment() {
+                var segmentPath;
+
+                if (index >= pathChain.length) {
+                    deferred.resolve(this.folderExistsInTree(path));
+                    return;
+                }
+
+                segmentPath = pathChain[index];
+
+                if (!this.folderExistsInTree(segmentPath)) {
+                    deferred.resolve(false);
+                    return;
+                }
+
+                this.openNodeAsync(segmentPath)
+                    .always(function () {
+                        index++;
+                        processNextSegment.call(this);
+                    }.bind(this));
             }
+
+            processNextSegment.call(this);
+
+            return deferred.promise();
+        },
+
+        /**
+         * Open node and resolve once jstree processes lazy children.
+         *
+         * @param {String} path
+         * @returns {jQuery.Promise}
+         */
+        openNodeAsync: function (path) {
+            var deferred = $.Deferred(),
+                tree = $(this.directoryTreeSelector).jstree(true);
+
+            if (!tree || !tree.get_node(path)) {
+                deferred.resolve(false);
+
+                return deferred.promise();
+            }
+
+            tree.open_node(path, function (node, status) {
+                deferred.resolve(status !== false);
+            });
+
+            return deferred.promise();
+        },
+
+        /**
+         * Convert path string into cumulative segments:
+         * 'a/b/c' -> ['a', 'a/b', 'a/b/c'].
+         *
+         * @param {String} path
+         * @returns {Array}
+         */
+        getPathChain: function (path) {
+            var segments = _.filter(path.split('/'), function (segment) {
+                    return segment !== '';
+                }),
+                pathChain = [];
+
+            $.each(segments, function (index) {
+                pathChain.push(segments.slice(0, index + 1).join('/'));
+            });
+
+            return pathChain;
         },
 
         /**
@@ -415,15 +555,27 @@ define([
             filters = $.extend(true, filters, applied);
             filters.path = path;
             this.filterChips().set('applied', filters);
+
+            if (!_.isUndefined(this.bookmarks()) && _.isFunction(this.bookmarks().store)) {
+                this.bookmarks().store('current');
+            }
         },
 
         /**
          * Reload jstree and update jstree events
          */
-        reloadJsTree: function () {
+        reloadJsTree: function (loadWholeTree) {
             var deferred = $.Deferred();
 
-            this.getJsonTree().then(function (data) {
+            if (this.isLazyTreeMode() && !loadWholeTree) {
+                $(this.directoryTreeSelector).jstree(true).refresh(false, true);
+                this.setJsTreeReloaded(true);
+                deferred.resolve();
+
+                return deferred.promise();
+            }
+
+            this.getJsonTree(true).then(function (data) {
                 $(this.directoryTreeSelector).jstree(true).settings.core.data = data;
                 $(this.directoryTreeSelector).jstree(true).refresh(false, true);
                 this.setJsTreeReloaded(true);
@@ -436,13 +588,17 @@ define([
         /**
          * Get json data for jstree
          */
-        getJsonTree: function () {
-            var deferred = $.Deferred();
+        getJsonTree: function (loadWholeTree) {
+            var deferred = $.Deferred(),
+                requestData = {
+                    loadWholeTree: loadWholeTree ? 1 : 0
+                };
 
             $.ajax({
                 url: this.getDirectoryTreeUrl,
                 type: 'GET',
                 dataType: 'json',
+                data: requestData,
 
                 /**
                  * Success handler for request
@@ -474,6 +630,28 @@ define([
          * @param {Array} data
          */
         createTree: function (data) {
+            var treeData = data;
+
+            if (this.isLazyTreeMode()) {
+                treeData = {
+                    url: this.getDirectoryTreeUrl,
+                    type: 'GET',
+
+                    /**
+                     * Return data payload for on-demand loading.
+                     *
+                     * @param {Object} node
+                     * @returns {Object}
+                     */
+                    data: function (node) {
+                        return {
+                            path: node.id,
+                            loadWholeTree: 0
+                        };
+                    }
+                };
+            }
+
             // jscs:disable requireCamelCaseOrUpperCaseIdentifiers
             $(this.directoryTreeSelector).jstree({
                 plugins: [],
@@ -482,7 +660,7 @@ define([
                     cascade: ''
                 },
                 core: {
-                    data: data,
+                    data: treeData,
                     check_callback: true,
                     themes: {
                         dots: false
