Firestore 获取数据性能缓慢问题

85

相对于实时数据库,我在Firestore检索存储在文档中的基本数据时遇到了性能缓慢的问题,速度比例为1/10。

使用Firestore,在第一次调用时平均需要3000毫秒。

 this.db.collection(‘testCol’)
   .doc(‘testDoc’)
   .valueChanges().forEach((data) => {
     console.log(data);//3000 ms later
 });

使用实时数据库,第一次调用平均需要300毫秒

 this.db.database.ref(‘/test’).once(‘value’).then(data => {
     console.log(data); //300ms later
 });

这是网络控制台的截图:

Firestore slow performance issue get Data

我正在运行Javascript SDK v4.50和AngularFire2 v5.0 rc.2。

有人遇到过这个问题吗?


如果您进行第二次调用(到另一个文档/集合),您会看到什么性能表现?如果不使用angularfire,是否仍会出现相同的问题? - Sam Stern
1
我有类似的经验。首先,onSnapShot需要很长时间 - 对于一些用户来说,需要高达2分钟的时间,这使我们的应用程序无法使用。 - wizloc
同样的问题,使用简单的collection.get(document) 需要1.5分钟。 - aMarCruz
现在在安卓中,使用Firebase实时数据库(启用离线模式)在第一次运行时也会很慢。因此我被迫使用云函数来进行REST请求,这样速度更快。 - Faruk
我有一个包含1个文档和3个字段的集合,没有特殊查询(只是返回所有内容),但需要大约3秒才能返回数据?非常担心如果没有工作要做时这就是性能表现,是否值得投入更多时间。 - Dominic
显示剩余7条评论
5个回答

49

更新:2018年2月12日 - iOS Firestore SDK v0.10.0

与其他评论者类似,我也注意到第一次获取请求的响应速度较慢(后续请求大约需要100毫秒)。对我来说并不像30秒那么糟糕,但是在我有良好的连接时可能需要2-3秒,这足以在应用程序启动时提供不良用户体验。

Firebase表示他们已经意识到这个“冷启动”问题,并且正在为其寻求长期解决方案-不幸的是没有时间表。我认为当我连接质量差时,可能需要很长时间(超过30秒)才能从缓存中读取获取请求,这是一个单独的问题。

在Firebase解决所有这些问题之前,我开始使用新的disableNetwork()enableNetwork()方法(在Firestore v0.10.0中可用)手动控制Firebase的在线/离线状态。尽管我必须在我的代码中非常小心地使用它,因为有一个Firestore错误,可以在某些情况下导致崩溃。


更新:2017年11月15日 - iOS Firestore SDK v0.9.2

似乎已经修复了性能缓慢的问题。我重新运行了下面描述的测试,现在Firestore返回100个文档所需的时间似乎始终稳定在约100毫秒左右。

不确定这是最新SDK v0.9.2中的修复还是后端的修复(或两者都有),但我建议每个人都更新他们的Firebase pods。我的应用程序响应速度明显更快-类似于Realtime DB。


我还发现Firestore比Realtime DB慢得多,特别是在读取大量文档时。

使用最新的iOS Firestore SDK v0.9.0进行更新测试:

我使用iOS Swift在RTDB和Firestore中设置了一个测试项目,并在每个数据库上运行了100个连续读取操作。对于RTDB,我测试了每个100个顶级节点上的observeSingleEvent和observe方法。对于Firestore,我在TestCol集合的每个100个文档中使用了getDocument和addSnapshotListener方法。我打开和关闭了磁盘持久性选项,并请参考附图,该图显示了每个数据库的数据结构。

我在同一设备和稳定的wifi网络上为每个数据库运行了10次测试。在每次新运行之前,销毁现有的观察者和监听器。

Realtime DB observeSingleEvent方法:

func rtdbObserveSingle() {

    let start = UInt64(floor(Date().timeIntervalSince1970 * 1000))
    print("Started reading from RTDB at: \(start)")

    for i in 1...100 {
        Database.database().reference().child(String(i)).observeSingleEvent(of: .value) { snapshot in
            let time = UInt64(floor(Date().timeIntervalSince1970 * 1000))
            let data = snapshot.value as? [String: String] ?? [:]
            print("Data: \(data). Returned at: \(time)")
        }
    }
}

实时数据库观察方法:

func rtdbObserve() {

    let start = UInt64(floor(Date().timeIntervalSince1970 * 1000))
    print("Started reading from RTDB at: \(start)")

    for i in 1...100 {
        Database.database().reference().child(String(i)).observe(.value) { snapshot in
            let time = UInt64(floor(Date().timeIntervalSince1970 * 1000))
            let data = snapshot.value as? [String: String] ?? [:]
            print("Data: \(data). Returned at: \(time)")
        }
    }
}

Firestore getDocument方法:

func fsGetDocument() {

    let start = UInt64(floor(Date().timeIntervalSince1970 * 1000))
    print("Started reading from FS at: \(start)")

    for i in 1...100 {
        Firestore.firestore().collection("TestCol").document(String(i)).getDocument() { document, error in

            let time = UInt64(floor(Date().timeIntervalSince1970 * 1000))
            guard let document = document, document.exists && error == nil else {
                print("Error: \(error?.localizedDescription ?? "nil"). Returned at: \(time)")
                return
            }
            let data = document.data() as? [String: String] ?? [:]
            print("Data: \(data). Returned at: \(time)")
        }
    }
}

Firestore addSnapshotListener 方法:

func fsAddSnapshotListener() {

    let start = UInt64(floor(Date().timeIntervalSince1970 * 1000))
    print("Started reading from FS at: \(start)")

    for i in 1...100 {
        Firestore.firestore().collection("TestCol").document(String(i)).addSnapshotListener() { document, error in

            let time = UInt64(floor(Date().timeIntervalSince1970 * 1000))
            guard let document = document, document.exists && error == nil else {
                print("Error: \(error?.localizedDescription ?? "nil"). Returned at: \(time)")
                return
            }
            let data = document.data() as? [String: String] ?? [:]
            print("Data: \(data). Returned at: \(time)")
        }
    }
}
每个方法实际上在执行时都会打印以毫秒为单位的Unix时间戳,并在每个读取操作返回时打印另一个Unix时间戳。我使用初始时间戳和最后一个时间戳之间的差异来返回结果。
结果-磁盘持久化已禁用: Disk persistence disabled 结果-磁盘持久化已启用: Disk persistence enabled 数据结构: Data Structure 当Firestore的getDocument/addSnapshotListener方法被卡住时,它似乎会卡住大约是30秒的倍数。也许这可以帮助Firebase团队隔离SDK中的问题所在?

3
Firestore更昂贵且速度慢得多。希望Firebase团队能看到这一点。 - Faruk
7
[Firebaser here] 感谢您花时间提供如此详细的数据,我们非常感激。问题不在于系统“较慢”,而是有一小部分查询会卡住或需要很长时间才能返回结果。我们即将推出一些修复措施,相信可以改善情况。 - Sam Stern
1
感谢您的更新。我已经添加了最新Firestore SDK v0.9.0的一些新结果,这可能有助于您的团队隔离问题的来源。 我还遇到了快照监听器的另一个问题:https://stackoverflow.com/questions/46710371/firestore-ios-snapshot-listener-error-the-referenced-transaction-has-expired 根本原因可能与此主题有关,但如果Firebase团队能够查看它,那将是非常好的。非常感谢! - Saul
3
我们在Web SDK上也遇到了“卡住”的查询问题。在v4.8.0版本中,会出现10-20秒的停滞,然后数据才会到达。 - DarkNeuron
1
我最近注意到这个问题并向Firebase报告了。他们已经意识到“冷启动”问题并正在努力修复。与此同时,我正在尝试上面更新中详细介绍的解决方法,并且我已经在使用中取得了不同程度的成功。 - Saul
显示剩余11条评论

23

更新日期:2018年3月2日

看起来这是一个已知的问题,Firestore的工程师正在努力修复。在与Firestore的一位工程师进行了几次电子邮件交流和代码共享后,他对此问题的回复如下。

“你说得很正确。经过进一步检查,getDocuments()API的缓慢性是Cloud Firestore beta中已知的行为。我们的工程师已经意识到这个被标记为“冷启动”的性能问题,但请放心,我们正在尽最大努力改进Firestore查询性能。

我们已经在着手长期解决方案,但目前我不能分享任何时间表或具体细节。虽然Firestore仍处于beta版本,但预计还会有更多的改进出现。”

所以希望这个问题很快就会被解决。


使用Swift / iOS

经过约3天的处理,似乎问题确实出在get(),即.getDocuments和.getDocument。我原本以为这些问题导致极端而间歇性的延迟,但事实并非如此:

  1. 网络连接不太好
  2. 通过循环调用.getDocuments()重复调用
  3. 链接get()调用
  4. Firestore冷启动
  5. 获取多个文档(仅获取1个小文档引起20秒延迟)
  6. 缓存(我禁用了离线持久性,但这没有任何作用。)

我排除了所有这些可能性,因为我注意到这个问题并不是在我进行的每一个Firestore数据库调用中都发生。只有使用get()检索时才会出现。出于好奇,我将.getDocument替换为.addSnapshotListener来检索我的数据,结果惊人,每次都能立即检索,包括第一次调用。没有冷启动。到目前为止,.addSnapshotListener没有出现任何问题,只有getDocument(s)。

现在,当时间紧迫时,我只是放弃使用.getDocument()并将其替换为.addSnapshotListener,然后使用

for document in querySnapshot!.documents{
// do some magical unicorn stuff here with my document.data()
}

...以便在Firestore解决此问题之前继续移动。


我也看到了同样的行为,但只在Android上。目前我也回退到快照。但如果get查询的性能是一致的,那就太好了。 - sowdri
我也发现FirebaseUI Recycler Adapter的性能很慢,它使用addSnapshotListener。 - Jeff Padgett
这个“冷启动”问题还存在吗?我有点困惑你三月份的更新提到Firebase工程师已经意识到了他们所标记的“冷启动”问题,因为在你最初的回答中,你写道你已经排除了“4. Firestore冷启动”可能是问题的原因? - trollkotze
我仍然看到Android的性能缓慢和许多内存问题。你们计划提供任何更新来解决这个问题吗? - hiten pannu
最新版本的Firestore在iOS上仍然存在这个问题,使用快照监听器却非常顺利。真是个了不起的发现。 - John Doe

12

近三年过去了,Firestore已经不再是测试版,但我可以证实这个可怕的问题仍然存在。在我们的移动应用程序中,我们使用JavaScript/Node.js Firebase客户端。经过多次测试,我们发现启动时间约为10秒,其中70%的时间都归因于Firebase和Firestore的性能和冷启动问题:

  • firebase.auth().onAuthStateChanged() 大约需要1.5到2秒才触发,已经很糟糕了。
  • 如果它返回一个用户,我们使用其ID从Firestore获取用户文档。这是对Firestore的第一次调用,相应的get()需要4到5秒。后续获取(同一文档或其他文档)大约需要500毫秒。

因此,用户初始化总共需要6-7秒,完全无法接受。而且我们无法解决它。我们不能测试禁用持久性,因为在JavaScript客户端中没有这样的选项,持久性默认始终启用,因此不调用enablePersistence()不会改变任何事情。


有什么解决这个问题的方法吗?指出在哪里寻找答案的一些提示将不胜感激。 - bjornl
在 JavaScript 客户端中,默认情况下已关闭:对于 Web,离线持久性默认处于禁用状态。要启用持久性,请调用 enablePersistence 方法 但是我可以确认,如果将其关闭,我们的初始请求时间从大约 8 秒降至约 500 毫秒左右。https://firebase.google.com/docs/firestore/manage-data/enable-offline - pat
正确,在JavaScript中,默认情况下它是关闭的,我上面提到的时间都是使用默认设置。而在我们的情况下,我们需要新鲜和更新的用户资料数据,因此使用持久性不是一个选项。 - JPJ

8
我遇到了这个问题,直到今天早上。我的iOS/Swift通过Firestore查询需要约20秒才能完成一个简单的、完全索引的查询,对于返回1个项目的非比例查询时间,一直到3,000个项目。
我的解决方法是禁用离线数据持久性。在我的情况下,它不适合我们的Firestore数据库的需求,因为其大部分数据每天都会更新。
iOS和Android用户默认启用此选项,而Web用户默认禁用此选项。如果您正在查询大量文档的集合,它会使Firestore看起来非常缓慢。基本上,它缓存您查询的任何数据(以及您查询的任何集合——我相信它会缓存其中的所有文档),这可能导致内存使用量很高。
在我的情况下,它会导致每次查询都等待很长时间,直到设备缓存所需的数据为止,因此对于从完全相同的集合返回的项目数量增加,查询时间是非比例的。这是因为每个查询都需要相同的时间来缓存集合。 离线数据-来自Cloud Firestore Docs 我进行了一些基准测试,以显示这种效果(启用离线持久性),但使用.limit参数返回不同数量的项目的相同查询集合:

Benchmarks 现在返回100个项目(禁用离线持久性),我的查询只需要不到1秒钟就能完成。

我的Firestore查询代码如下:

let db = Firestore.firestore()
self.date = Date()
let ref = db.collection("collection").whereField("Int", isEqualTo: SomeInt).order(by: "AnotherInt", descending: true).limit(to: 100)
ref.getDocuments() { (querySnapshot, err) in
    if let err = err {
        print("Error getting documents: \(err)")
    } else {
        for document in querySnapshot!.documents {
            let data = document.data()
            //Do things
        }
        print("QUERY DONE")
        let currentTime = Date()
        let components = Calendar.current.dateComponents([.second], from: self.date, to: currentTime)
        let seconds = components.second!
        print("Elapsed time for Firestore query -> \(seconds)s")
        // Benchmark result
    }
}

这些测试数字来自Web、iOS还是Android? - Sam Stern
基准测试来自iOS,尽管我期望在所有平台上都能获得性能提升 - 这取决于您查询的集合大小。 - Hendies
没意识到你在 Firebase 团队!我发现这个问题的线索是内存使用率会急剧上升(600-700mb RAM),而我原本假设我们的集合已经被存储在缓存中。查询总是会挂起,然后在内存逐渐增长并达到相同点(大约 700mb)时才完成。禁用持久性后,这种效果消失了,我们的内存保持在预期范围内(100-150mb),同时返回结果超级快。如果您需要更多信息,请询问。 - Hendies
哦,那非常有趣。你能否创建一个示例Xcode项目来复制它并通过电子邮件发送给我?如果可以的话,请发送至samstern@gmail.com。我很想仔细研究一下。 - Sam Stern
@SamStern:这已经有一段时间了,但我在Android上面临着完全相同的问题。你有什么线索可以解释这是什么原因吗?我尝试构建了一个最小化的项目,但那个项目没有出现这个问题! - rednuht

1

目前我正在使用模拟器和真实的Android手机华为P8,进行研究工作。

Firestore和Cloud Storage都让我头疼,因为当我进行第一次document.get()和第一次storage.getDownloadUrl()时,响应速度非常慢。

每个请求都需要超过60秒的响应时间。这种缓慢的响应只会在真实的Android手机上出现,而不会在模拟器中出现。还有另外一件奇怪的事情,第一次遇到之后,其余的请求都很顺畅。

以下是我遇到缓慢响应的简单代码。

var dbuserref = dbFireStore.collection('user').where('email','==',email);
const querySnapshot = await dbuserref.get();

var url = await defaultStorage.ref(document.data().image_path).getDownloadURL();

我还发现了一个正在研究相同问题的链接。 https://reformatcode.com/code/android/firestore-document-get-performance

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接